From 705699b2d2f532784ed39583c030e9f72263caff Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Fri, 16 Aug 2019 19:21:22 +0200 Subject: move selenium to contrib --- contrib/selenium/README | 10 ++ contrib/selenium/chrome_responsive.py | 47 +++++ contrib/selenium/withdraw_buy.py | 317 ++++++++++++++++++++++++++++++++++ 3 files changed, 374 insertions(+) create mode 100644 contrib/selenium/README create mode 100755 contrib/selenium/chrome_responsive.py create mode 100755 contrib/selenium/withdraw_buy.py (limited to 'contrib') diff --git a/contrib/selenium/README b/contrib/selenium/README new file mode 100644 index 000000000..0f56746fa --- /dev/null +++ b/contrib/selenium/README @@ -0,0 +1,10 @@ + +Directory containing testscases for testing the wallet with Selenium ChromeDriver. + + +Useful links: + +* ChromeDriver Pythonic documentation: + http://seleniumhq.github.io/selenium/docs/api/py/index.html +* Which fields (and which values) the 'loggingPrefs' capability expects: + https://github.com/SeleniumHQ/selenium/wiki/DesiredCapabilities#loggingpreferences-json-object diff --git a/contrib/selenium/chrome_responsive.py b/contrib/selenium/chrome_responsive.py new file mode 100755 index 000000000..483e2dbab --- /dev/null +++ b/contrib/selenium/chrome_responsive.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 + +""" +Stub used to just test how selenium triggers chrome +""" + +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 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__) + +def client_setup(args): + """Return a dict containing the driver and the extension's id""" + co = webdriver.ChromeOptions() + cap = co.to_capabilities() + cap['loggingPrefs'] = {'driver': 'INFO', 'browser': 'INFO'} + + if args.remote: + client = webdriver.Remote(desired_capabilities=cap, command_executor=args.remote) + else: + client = webdriver.Chrome(desired_capabilities=cap) + client.get('https://gnunet.org/') + html = client.find_element(By.TAG_NAME, "html") + return client + +parser = argparse.ArgumentParser() +parser.add_argument('--remote', help="Points webdriver.Remote at URI", metavar="URI", type=str, dest="remote") +args = parser.parse_args() +ret = client_setup(args) +logger.info("Chromium is responsive") +for entry in ret.get_log("browser"): + print(entry.get("message")) +time.sleep(3) +ret.close() diff --git a/contrib/selenium/withdraw_buy.py b/contrib/selenium/withdraw_buy.py new file mode 100755 index 000000000..2a2764690 --- /dev/null +++ b/contrib/selenium/withdraw_buy.py @@ -0,0 +1,317 @@ +#!/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)) + +class TestContext(): + def __init__(self, client): + self.client = client + self.wait = WebDriverWait(client, 20) + +def kill_display(): + if hasattr(display, "old_display_var"): + display.stop() + +# All the operations we need upon errors. +def abort(client): + client.quit() + kill_display() + 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: + client = webdriver.Chrome(desired_capabilities=cap) + 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 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 make_donation(ctx, amount_menuentry): + """Make donation at donations.test.taler.net. Assume + the wallet has coins. Just donate to the default receiver""" + ctx.client.get(parse.urljoin(taler_baseurl, "donations")) + try: + form = ctx.wait.until(EC.visibility_of_element_located((By.TAG_NAME, + "form"))) + except (NoSuchElementException, TimeoutException): + logger.error('No donation form found') + return False + xpath_menu = '//select[@id="taler-donation"]' + try: + dropdown = ctx.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 = ctx.wait.until(EC.visibility_of(option)) + option.click() + break + except (NoSuchElementException, TimeoutException): + logger.error("amount '" + str(amount_value) + "\ + ' is not offered by this shop to donate") + return False + form.submit() + try: + confirm_taler = ctx.wait.until(EC.element_to_be_clickable((By.ID, + "select-payment-method"))) + except (NoSuchElementException, TimeoutException): + logger.error('Could not trigger contract on donation shop') + return False + confirm_taler.click() + try: + confirm_pay = ctx.wait.until(EC.element_to_be_clickable((By.XPATH, + "//button[@class='pure-button button-success']"))) + except (NoSuchElementException, TimeoutException): + logger.error('Could not confirm payment on donation shop') + return False + confirm_pay.click() + return True + +# Check if the current page is displaying the article +# whose title is 'title'. + +def check_article(ctx, title): + try: + ctx.wait.until(EC.visibility_of_element_located((By.XPATH, + "//h1[contains(., '%s')]" % title.replace("_", " ")))) + except (NoSuchElementException, TimeoutException): + logger.error("Article '%s' not shown on this (%s) page\ + " % (title, ctx.client.current_url)) + return False + return True + + +def buy_article(ctx, 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: + ctx.client.get(fulfillment_url) + if check_article(ctx, title): + return fulfillment_url + return False + ctx.client.get(parse.urljoin(taler_baseurl, "shop")) + try: + teaser = ctx.wait.until(EC.element_to_be_clickable((By.XPATH, + "//h3/a[@href=\"/essay/%s\"]" % title))) + ctx.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) + return False + time.sleep(1) + try: + confirm_pay = ctx.wait.until(EC.element_to_be_clickable((By.XPATH, + "//button[@class='pure-button button-success']"))) + except (NoSuchElementException, TimeoutException): + logger.error('Could not confirm contract on blog (timed out)') + return False + confirm_pay.click() + time.sleep(3) + if not check_article(ctx, title): + return False + return ctx.client.current_url + +def register(ctx): + """Register a new user to the bank delaying its execution + until the profile page is shown""" + ctx.client.get(parse.urljoin(taler_baseurl, "bank")) + try: + register_link = ctx.wait.until(EC.element_to_be_clickable((By.XPATH, + "//a[@href='/accounts/register/']"))) + except (NoSuchElementException, TimeoutException): + logger.error("Could not find register link on bank's homepage") + return False + register_link.click() + try: + ctx.wait.until(EC.visibility_of_element_located((By.TAG_NAME, "form"))) + except (NoSuchElementException, TimeoutException): + logger.error("Register form not found") + return False + register = """\ + var form = document.getElementsByTagName('form')[0]; + form.username.value = '%s'; + form.password.value = 'test'; + form.submit(); + """ % str(int(time.time())) + ctx.client.execute_script(register) + try: + ctx.wait.until(EC.element_to_be_clickable((By.ID, + "select-exchange"))) + except (NoSuchElementException, TimeoutException): + logger.error("Selecting exchange impossible") + return False + return True + + +def withdraw(ctx, amount_menuentry): + """Register and withdraw (1) KUDOS for a fresh user""" + # trigger withdrawal button + try: + button = ctx.wait.until(EC.element_to_be_clickable((By.ID, + "select-exchange"))) + except (NoSuchElementException, TimeoutException): + logger.error("Selecting exchange impossible") + return False + xpath_menu = '//select[@id="reserve-amount"]' + try: + # No need to wait: if 'button' above exists, then + # menu elements do. + dropdown = ctx.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 = ctx.wait.until(EC.visibility_of(option)) + option.click() + break + except (NoSuchElementException, TimeoutException): + logger.error("amount '" + str(amount_value) + "' \ + is not offered by this bank to withdraw") + return False + button.click() + # Confirm exchange (in-wallet page) + try: + accept_exchange = ctx.wait.until(EC.element_to_be_clickable((By.XPATH, + "//button[@class='pure-button button-success']"))) + except (NoSuchElementException, TimeoutException): + logger.error("Could not confirm exchange") + return False + accept_exchange.click() + try: + answer = ctx.wait.until(EC.element_to_be_clickable((By.XPATH, + "//input[@name='pin_0']"))) + question = ctx.wait.until(EC.element_to_be_clickable((By.XPATH, + "//span[@class='captcha-question']/div"))) + + except (NoSuchElementException, TimeoutException): + logger.error("Captcha page unavailable or malformed") + return False + 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: + # No need to wait, if CAPTCHA elements exists + # then submitting button has to. + form = ctx.client.find_element(By.TAG_NAME, "form") + except (NoSuchElementException, TimeoutException): + logger.error("Could not submit captcha answer") + return False + form.submit() + # check outcome + try: + ctx.wait.until(EC.presence_of_element_located((By.CLASS_NAME, + "informational-ok"))) + except (NoSuchElementException, TimeoutException): + logger.error("Withdrawal not completed") + return False + return True + +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() + +ctx = TestContext(client_setup(args)['client']) + +if not register(ctx): + print_log(ctx.client) + abort(ctx.client) + +if not withdraw(ctx, "10.00 TESTKUDOS"): + print_log(ctx.client) + abort(ctx.client) + +fulfillment_url_25 = buy_article(ctx, + "25._The_Danger_of_Software_Patents") + +if not fulfillment_url_25: + print_log(ctx.client) + abort(ctx.client) + +ctx.client.delete_all_cookies() + +if not buy_article(ctx, "25._The_Danger_of_Software_Patents", + fulfillment_url_25): + print_log(ctx.client) + logger.error("Could not replay payment") + abort(ctx.client) + +if not make_donation(ctx, "1.0 TESTKUDOS"): + print_log(ctx.client) + abort(ctx.client) + +ctx.client.quit() +kill_display() +sys.exit(0) -- cgit v1.2.3