#!/usr/bin/env python3 """ Tests for the wallet. It looks for an env variable called TALER_BASEURL where it appends "/banks" etc. in order to find bank and shops. If not found, it defaults to https://test.taler.net/ """ from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.ui import WebDriverWait, Select from selenium.common.exceptions import NoSuchElementException, TimeoutException from selenium.webdriver.common.action_chains import ActionChains from pyvirtualdisplay import Display from urllib import parse import argparse import time import logging import sys import os import re import json logging.basicConfig(format='%(levelname)s: %(message)s', level=logging.INFO) logger = logging.getLogger(__name__) taler_baseurl = os.environ.get('TALER_BASEURL', 'https://test.taler.net/') display = Display(visible=0, size=(1024, 768)) def abort(client): client.quit() if hasattr(display, "old_display_var"): print("Kill display") display.stop() sys.exit(1) def client_setup(args): """Return a dict containing the driver and the extension's id""" co = webdriver.ChromeOptions() if args.ext: co.add_extension(args.ext) elif args.extdir: co.add_argument("load-extension=%s" % args.extdir) else: logger.error("Provide one between '--ext' and '--ext-unpacked'") sys.exit(1) cap = co.to_capabilities() cap['loggingPrefs'] = {'driver': 'INFO', 'browser': 'INFO'} if not args.withhead: display.start() if args.remote: client = webdriver.Remote(desired_capabilities=cap, command_executor=args.remote) else: logger.info("About to get chromed") client = webdriver.Chrome(desired_capabilities=cap) logger.info("Got chromed") client.get('https://taler.net') listener = """\ document.addEventListener('taler-query-id-result', function(evt){ var html = document.getElementsByTagName('html')[0]; html.setAttribute('data-taler-wallet-id', evt.detail.id); }); var evt = new CustomEvent('taler-query-id'); document.dispatchEvent(evt); """ client.execute_script(listener) html = client.find_element(By.TAG_NAME, "html") ext_id = html.get_attribute('data-taler-wallet-id') logger.info("Extension ID: %s" % str(ext_id)) return {'client': client, 'ext_id': ext_id} def is_error(client): """Return True in case of errors in the browser, False otherwise""" for log_type in ['browser']: for log in client.get_log(log_type): if log['level'] is 'error': print(log['level'] + ': ' + log['message']) return True return False def print_log(client): print("--- Dumping browser log: ---") for log in client.get_log("browser"): print(log['level'] + ': ' + log['message']) print("--- End of browser log ---") def switch_base(): """If 'test' is in TALER_BASEURL, then make it be 'demo', and viceversa. Used to trig currency mismatch errors. It assumes that the https://{test,demo}.taler.net layout is being used""" global taler_baseurl url = parse.urlparse(taler_baseurl) if url[1] == 'test.taler.net': taler_baseurl = "https://demo.taler.net" if url[1] == 'demo.taler.net': taler_baseurl = "https://test.taler.net" def make_donation(client, amount_menuentry=None): """Make donation at donations.test.taler.net. Assume the wallet has coins. Just donate to the default receiver""" client.get(parse.urljoin(taler_baseurl, "donations")) try: form = client.find_element(By.TAG_NAME, "form") except NoSuchElementException: logger.error('No donation form found') abort(client) if amount_menuentry: xpath_menu = '//select[@id="taler-donation"]' try: dropdown = client.find_element(By.XPATH, xpath_menu) for option in dropdown.find_elements_by_tag_name("option"): if option.get_attribute("innerHTML") == amount_menuentry: option = WebDriverWait(client, 10).until(EC.visibility_of(option)) logger.info("Picked donation %s" % option.text) option.click() break except NoSuchElementException: logger.error("value '" + str(amount_value) + "' is not offered by this shop to donate, please adapt it") abort(client) form.submit() # amount and receiver chosen wait = WebDriverWait(client, 10) try: confirm_taler = wait.until(EC.element_to_be_clickable((By.ID, "select-payment-method"))) logger.info("confirm_taler: %s" % confirm_taler.get_attribute("outerHTML")) except NoSuchElementException: logger.error('Could not trigger contract on donation shop') abort(client) confirm_taler.click() # Taler as payment option chosen try: confirm_pay = wait.until(EC.element_to_be_clickable((By.XPATH, "//button[@class='pure-button button-success']"))) except TimeoutException: logger.error('Could not confirm payment on donation shop') abort(client) confirm_pay.click() def check_article(client, title): try: client.find_element(By.XPATH, "//h1[contains(., '%s')]" % title.replace("_", " ")) except NoSuchElementException: logger.error("Article not correctly bought") client.quit() display.stop() return False return True def buy_article(client, title, fulfillment_url=None): """Buy article at shop.test.taler.net. Assume the wallet has coins. Return False if some error occurs, the fulfillment URL otherwise""" if fulfillment_url: client.get(fulfillment_url) if check_article(client, title): return fulfillment_url return False client.get(parse.urljoin(taler_baseurl, "shop")) wait = WebDriverWait(client, 20) try: teaser = wait.until(EC.element_to_be_clickable((By.XPATH, "//h3/a[@href=\"/essay/%s\"]" % title))) logger.info("Scrolling to article position: %s", teaser.location['y']) client.execute_script("window.scrollBy(30, %s)" % teaser.location['y']) teaser.click() except (NoSuchElementException, TimeoutException) as e: logger.error("Could not choose chapter '%s'" % title) abort(client) time.sleep(1) try: confirm_pay = wait.until(EC.element_to_be_clickable((By.XPATH, "//button[@class='pure-button button-success']"))) logger.info("Pay button turned clickable") except TimeoutException: logger.error('Could not confirm payment on blog (timed out)') print_log(client) abort(client) confirm_pay.click() time.sleep(3) if not check_article(client, title): return False return client.current_url def register(client): """Register a new user to the bank delaying its execution until the profile page is shown""" client.get(parse.urljoin(taler_baseurl, "bank")) try: register_link = client.find_element(By.XPATH, "//a[@href='/accounts/register/']") except NoSuchElementException: logger.error("Could not find register link on bank's homepage") abort(client) register_link.click() try: client.find_element(By.TAG_NAME, "form") except NoSuchElementException: logger.error("Register form not found") abort(client) register = """\ var form = document.getElementsByTagName('form')[0]; form.username.value = '%s'; form.password.value = 'test'; form.submit(); """ % str(int(time.time())) # need fresh username client.execute_script(register) # need implicit wait to be set up try: button = client.find_element(By.ID, "select-exchange") except NoSuchElementException: logger.error("Selecting exchange impossible") abort(client) # when button is gotten, the browser is in the profile page # so the function can return if not is_error(client): logger.info('Correctly registered at bank') else: logger.error('User not registered at bank') def withdraw(client, amount_menuentry=None): """Register and withdraw (1) KUDOS for a fresh user""" register(client) logger.info("Withdrawing..") wait = WebDriverWait(client, 10) # trigger withdrawal button try: button = client.find_element(By.ID, "select-exchange") except NoSuchElementException: logger.error("Selecting exchange impossible") abort(client) if amount_menuentry: xpath_menu = '//select[@id="reserve-amount"]' try: dropdown = client.find_element(By.XPATH, xpath_menu) for option in dropdown.find_elements_by_tag_name("option"): if option.get_attribute("innerHTML") == amount_menuentry: option = WebDriverWait(client, 10).until(EC.visibility_of(option)) option.click() break except NoSuchElementException: logger.error("value '" + str(amount_value) + "' is not offered by this bank to withdraw, please adapt it") abort(client) # confirm amount logger.info("About to confirm amount") button.click() logger.info("Exchange confirmation refreshed") # Confirm exchange (in-wallet page) try: logger.info("Polling for the button") accept_exchange = wait.until(EC.element_to_be_clickable((By.XPATH, "//button[@class='pure-button button-success']"))) except TimeoutException: logger.error("Could not confirm exchange") abort(client) # This click returns the captcha page (put wait?) logger.info("About to confirm exchange") accept_exchange.click() try: accept_fees = wait.until(EC.element_to_be_clickable((By.XPATH, "//button[1]"))) except TimeoutException: logger.error("Could not accept fees") abort(client) accept_fees.click() # Properly wait for the next page to be loaded? try: answer = client.find_element(By.XPATH, "//input[@name='pin_0']") question = client.find_element(By.XPATH, "//span[@class='captcha-question']/div") except NoSuchElementException: logger.error("Captcha page not gotten or malformed") abort(client) questionTok = question.text.split() op1 = int(questionTok[2]) op2 = int(questionTok[4]) res = {'+': op1 + op2, '-': op1 - op2, u'\u00d7': op1 * op2} answer.send_keys(res[questionTok[3]]) try: form = client.find_element(By.TAG_NAME, "form") except NoSuchElementException: logger.error("Could not submit captcha answer (therefore trigger withdrawal)") abort(client) form.submit() # check outcome try: client.find_element(By.CLASS_NAME, "informational-ok") except NoSuchElementException: logger.error("Withdrawal not completed") abort(client) logger.info("Withdrawal completed") parser = argparse.ArgumentParser() parser.add_argument('--ext', help="packed extension (.crx file)", metavar="CRX", type=str, dest="ext") parser.add_argument('--ext-unpacked', help="loads unpacked extension from directory", metavar="EXTDIR", type=str, dest="extdir") parser.add_argument('--remote', help="Whether the test is to be run against URI, or locally", metavar="URI", type=str, dest="remote") parser.add_argument('--with-head', help="Graphically shows the browser (useful to debug)", action="store_true", dest="withhead") args = parser.parse_args() logger.info("testing against " + taler_baseurl) logger.info("Getting extension's ID..") ret = client_setup(args) logger.info("Creating the browser driver..") client = ret['client'] client.implicitly_wait(10) withdraw(client, "10.00 TESTKUDOS") # FIXME: wait for coins appropriately time.sleep(2) logger.info("Buying article..") fulfillment_url_25 = buy_article(client, "25._The_Danger_of_Software_Patents") fulfillment_url_41 = buy_article(client, "41._Avoiding_Ruinous_Compromises") client.delete_all_cookies() ret = buy_article(client, "25._The_Danger_of_Software_Patents", fulfillment_url_25) logger.info("Donating..") make_donation(client, "1.0 TESTKUDOS") # FIXME: better clearing the cache before replaying the payment logger.info("Payment replayed: '%s'" % ret) logger.info("Test passed") client.quit() if hasattr(display, "old_display_var"): display.stop() sys.exit(0)