diff --git a/.gitignore b/.gitignore index e347855..0e3eb68 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,10 @@ profiles.clj *.iml *.ipr *.log +*.pyc +*result.xml +.cache +.idea resources/contracts node_modules .DS_Store diff --git a/src/clj/commiteth/util/crypto_fiat_value.clj b/src/clj/commiteth/util/crypto_fiat_value.clj index abc0948..de95b09 100644 --- a/src/clj/commiteth/util/crypto_fiat_value.clj +++ b/src/clj/commiteth/util/crypto_fiat_value.clj @@ -1,27 +1,72 @@ (ns commiteth.util.crypto-fiat-value (:require [clj-http.client :as http] - [clojure.string :as str] + [mount.core :as mount] + [clojure.tools.logging :as log] + [commiteth.config :refer [env]] [clojure.data.json :as json])) -(defn get-token-usd-price +(defn fiat-api-provider [] + (env :fiat-api-provider :coinmarketcap)) + +(defn json-api-request [url] + (->> (http/get url) + (:body) + (json/read-str))) + + +(defn get-token-usd-price-cryptonator "Get current USD value for a token using cryptonator API" - [token] - (let [url (str "https://api.cryptonator.com/api/ticker/" + [tla] + (let [token (subs (str tla) 1) + url (str "https://api.cryptonator.com/api/ticker/" token "-usd") - m (->> (http/get url) - (:body) - (json/read-str))] + m (json-api-request url)] (-> (get-in m ["ticker" "price"]) (read-string)))) +(def tla-to-id-mapping (atom {})) + +(defn make-tla-to-id-mapping + "Coinmarketcap API uses it's own IDs for tokens instead of TLAs" + [] + (let [data (json-api-request "https://api.coinmarketcap.com/v1/ticker/?limit=0")] + (into {} (map + (fn [x] [(keyword (get x "symbol")) (get x "id")]) + data)))) + +(defn get-token-usd-price-coinmarketcap + "Get current USD value for a token using coinmarketcap API" + [tla] + (let [token-id (get @tla-to-id-mapping tla) + url (format "https://api.coinmarketcap.com/v1/ticker/%s" token-id) + data (json-api-request url)] + (-> (first data) + (get "price_usd") + (read-string)))) + +(defn- get-price-fn [] + (let [fns {:cryptonator get-token-usd-price-cryptonator + :coinmarketcap get-token-usd-price-coinmarketcap}] + (get fns (fiat-api-provider)))) + (defn bounty-usd-value "Get current USD value of a bounty. bounty is a map of token-tla (keyword) to value" [bounty] - (reduce + (map (fn [[token value]] - (let [tla (subs (str token) 1) - usd-price (get-token-usd-price tla)] - (* usd-price value))) - bounty))) + (let [get-token-usd-price (get-price-fn)] + (reduce + (map (fn [[tla value]] + (let [usd-price (get-token-usd-price tla)] + (* usd-price value))) + bounty)))) + + +(mount/defstate + crypto-fiat-util + :start + (do + (reset! tla-to-id-mapping (make-tla-to-id-mapping)) + (log/info "crypto-fiat-util started")) + :stop + (log/info "crypto-fiat-util stopped")) diff --git a/test/end-to-end/pages/base_element.py b/test/end-to-end/pages/base_element.py new file mode 100644 index 0000000..3da2cb8 --- /dev/null +++ b/test/end-to-end/pages/base_element.py @@ -0,0 +1,105 @@ +import logging +from selenium.webdriver.common.by import By +from selenium.common.exceptions import TimeoutException +from selenium.webdriver.support.wait import WebDriverWait +from selenium.webdriver.support import expected_conditions + + +class BaseElement(object): + + class Locator(object): + + def __init__(self, by, value): + self.by = by + self.value = value + + @classmethod + def xpath_selector(locator, value): + return locator(By.XPATH, value) + + @classmethod + def css_selector(locator, value): + return locator(By.CSS_SELECTOR, value) + + @classmethod + def id(locator, value): + return locator(By.ID, value) + + @classmethod + def name(locator, value): + return locator(By.NAME, value) + + def __str__(self, *args): + return "%s:%s" % (self.by, self.value) + + def __init__(self, driver): + self.driver = driver + self.locator = None + + @property + def name(self): + return self.__class__.__name__ + + def navigate(self): + return None + + def find_element(self): + logging.info('Looking for %s' % self.name) + return self.wait_for_element() + + def find_elements(self): + logging.info('Looking for %s' % self.name) + return self.driver.find_elements(self.locator.by, + self.locator.value) + + def wait_for_element(self, seconds=5): + return WebDriverWait(self.driver, seconds).until( + expected_conditions.presence_of_element_located((self.locator.by, self.locator.value))) + + def wait_for_clickable(self, seconds=5): + return WebDriverWait(self.driver, seconds).until( + expected_conditions.element_to_be_clickable((self.locator.by, self.locator.value))) + + def is_element_present(self, sec=5): + try: + self.wait_for_element(sec) + return True + except TimeoutException: + return False + + +class BaseEditBox(BaseElement): + + def __init__(self, driver): + super(BaseEditBox, self).__init__(driver) + + def send_keys(self, value): + self.find_element().send_keys(value) + logging.info('Type %s to %s' % (value, self.name)) + + def clear(self): + self.find_element().clear() + logging.info('Clear text in %s' % self.name) + + +class BaseText(BaseElement): + + def __init__(self, driver): + super(BaseText, self).__init__(driver) + + @property + def text(self): + text = self.find_element().text + logging.info('%s is %s' % (self.name, text)) + return text + + +class BaseButton(BaseElement): + + def __init__(self, driver): + super(BaseButton, self).__init__(driver) + + def click(self): + self.wait_for_clickable().click() + logging.info('Tap on %s' % self.name) + return self.navigate() diff --git a/test/end-to-end/pages/base_page.py b/test/end-to-end/pages/base_page.py new file mode 100644 index 0000000..de61cd4 --- /dev/null +++ b/test/end-to-end/pages/base_page.py @@ -0,0 +1,17 @@ +from datetime import datetime + + +class BasePageObject(object): + + def __init__(self, driver): + self.driver = driver + + def get_url(self, url): + self.driver.get(url) + + def refresh(self): + self.driver.refresh() + + @property + def time_now(self): + return datetime.now().strftime('%-m%-d%-H%-M%-S') diff --git a/test/end-to-end/pages/openbounty/bounties.py b/test/end-to-end/pages/openbounty/bounties.py new file mode 100644 index 0000000..91415fd --- /dev/null +++ b/test/end-to-end/pages/openbounty/bounties.py @@ -0,0 +1,25 @@ +from pages.base_page import BasePageObject +from pages.base_element import * + + +class BountiesHeader(BaseText): + + def __init__(self, driver): + super(BountiesHeader, self).__init__(driver) + self.locator = self.Locator.css_selector('.open-bounties-header') + + +class TopHuntersHeader(BaseText): + + def __init__(self, driver): + super(TopHuntersHeader, self).__init__(driver) + self.locator = self.Locator.css_selector('.top-hunters-header') + + +class BountiesPage(BasePageObject): + def __init__(self, driver): + super(BountiesPage, self).__init__(driver) + self.driver = driver + + self.bounties_header = BountiesHeader(self.driver) + self.top_hunters_header = TopHuntersHeader(self.driver) diff --git a/test/end-to-end/pages/openbounty/landing.py b/test/end-to-end/pages/openbounty/landing.py new file mode 100644 index 0000000..7886f90 --- /dev/null +++ b/test/end-to-end/pages/openbounty/landing.py @@ -0,0 +1,23 @@ +from pages.base_page import BasePageObject +from pages.base_element import * + + +class LoginButton(BaseButton): + def __init__(self, driver): + super(LoginButton, self).__init__(driver) + self.locator = self.Locator.id('button-login') + + def navigate(self): + from pages.thirdparty.github import GithubPage + return GithubPage(self.driver) + + +class LandingPage(BasePageObject): + def __init__(self, driver): + super(LandingPage, self).__init__(driver) + self.driver = driver + + self.login_button = LoginButton(self.driver) + + def get_landing_page(self): + self.driver.get('https://openbounty.status.im:444/') diff --git a/test/end-to-end/pages/thirdparty/github.py b/test/end-to-end/pages/thirdparty/github.py new file mode 100644 index 0000000..2585c91 --- /dev/null +++ b/test/end-to-end/pages/thirdparty/github.py @@ -0,0 +1,173 @@ +import time, pytest +from pages.base_element import * +from pages.base_page import BasePageObject + + +class EmailEditbox(BaseEditBox): + + def __init__(self, driver): + super(EmailEditbox, self).__init__(driver) + self.locator = self.Locator.id('login_field') + + +class PasswordEditbox(BaseEditBox): + + def __init__(self, driver): + super(PasswordEditbox, self).__init__(driver) + self.locator = self.Locator.id('password') + + +class SignInButton(BaseButton): + + def __init__(self, driver): + super(SignInButton, self).__init__(driver) + self.locator = self.Locator.name('commit') + + +class AuthorizeStatusOpenBounty(BaseButton): + def __init__(self, driver): + super(AuthorizeStatusOpenBounty, self).__init__(driver) + self.locator = self.Locator.css_selector('[data-octo-click="oauth_application_authorization"]') + + def navigate(self): + from pages.openbounty.bounties import BountiesPage + return BountiesPage(self.driver) + + +class PermissionTypeText(BaseText): + def __init__(self, driver): + super(PermissionTypeText, self).__init__(driver) + self.locator = self.Locator.css_selector('.permission-title') + + +class InstallButton(BaseButton): + def __init__(self, driver): + super(InstallButton, self).__init__(driver) + self.locator = self.Locator.css_selector('.btn-primary') + + +class OrganizationButton(BaseButton): + def __init__(self, driver): + super(OrganizationButton, self).__init__(driver) + self.locator = self.Locator.css_selector('[alt="@Org4"]') + + +class AllRepositoriesButton(BaseButton): + def __init__(self, driver): + super(AllRepositoriesButton, self).__init__(driver) + self.locator = self.Locator.id('install_target_all') + + +class IntegrationPermissionsGroup(BaseText): + def __init__(self, driver): + super(IntegrationPermissionsGroup, self).__init__(driver) + self.locator = self.Locator.css_selector('.integrations-permissions-group') + + +class NewIssueButton(BaseButton): + def __init__(self, driver): + super(NewIssueButton, self).__init__(driver) + self.locator = self.Locator.css_selector(".subnav [role='button']") + + +class IssueTitleEditBox(BaseEditBox): + def __init__(self, driver): + super(IssueTitleEditBox, self).__init__(driver) + self.locator = self.Locator.id("issue_title") + + +class LabelsButton(BaseButton): + def __init__(self, driver): + super(LabelsButton, self).__init__(driver) + self.locator = self.Locator.css_selector("button[aria-label='Apply labels to this issue']") + + class BountyLabel(BaseButton): + def __init__(self, driver): + super(LabelsButton.BountyLabel, self).__init__(driver) + self.locator = self.Locator.css_selector("[data-name='bounty']") + + class CrossButton(BaseButton): + def __init__(self, driver): + super(LabelsButton.CrossButton, self).__init__(driver) + self.locator = self.Locator.xpath_selector( + "//span[text()='Apply labels to this issue']/../*[@aria-label='Close']") + + +class SubmitNewIssueButton(BaseButton): + def __init__(self, driver): + super(SubmitNewIssueButton, self).__init__(driver) + self.locator = self.Locator.xpath_selector("//button[contains(text(), " + "'Submit new issue')]") + + +class ContractBody(BaseText): + def __init__(self, driver): + super(ContractBody, self).__init__(driver) + self.locator = self.Locator.xpath_selector("//tbody//p[contains(text(), " + "'Current balance: 0.000000 ETH')]") + + +class GithubPage(BasePageObject): + def __init__(self, driver): + super(GithubPage, self).__init__(driver) + + self.driver = driver + + self.email_input = EmailEditbox(self.driver) + self.password_input = PasswordEditbox(self.driver) + self.sign_in_button = SignInButton(self.driver) + + self.authorize_sob = AuthorizeStatusOpenBounty(self.driver) + self.permission_type = PermissionTypeText(self.driver) + + self.install_button = InstallButton(self.driver) + self.organization_button = OrganizationButton(self.driver) + self.all_repositories_button = AllRepositoriesButton(self.driver) + self.integration_permissions_group = IntegrationPermissionsGroup(self.driver) + + self.new_issue_button = NewIssueButton(self.driver) + self.issue_title_input = IssueTitleEditBox(self.driver) + self.labels_button = LabelsButton(self.driver) + self.bounty_label = LabelsButton.BountyLabel(self.driver) + self.cross_button = LabelsButton.CrossButton(self.driver) + self.submit_new_issue_button = SubmitNewIssueButton(self.driver) + self.contract_body = ContractBody(self.driver) + + def get_issues_page(self): + self.driver.get('https://github.com/Org4/nov13/issues') + + def get_sob_plugin_page(self): + self.driver.get('http://github.com/apps/status-open-bounty-app-test') + + def sign_in(self, email, password): + self.email_input.send_keys(email) + self.password_input.send_keys(password) + self.sign_in_button.click() + + def install_sob_plugin(self): + initial_url = self.driver.current_url + self.get_sob_plugin_page() + self.install_button.click() + self.organization_button.click() + self.all_repositories_button.click() + self.install_button.click() + self.driver.get(initial_url) + + def create_new_bounty(self): + self.get_issues_page() + self.new_issue_button.click() + self.issue_title_input.send_keys('auto_test_bounty_%s' % self.time_now) + self.labels_button.click() + self.bounty_label.click() + self.cross_button.click() + self.submit_new_issue_button.click() + + def get_deployed_contract(self, wait=120): + for i in range(wait): + self.refresh() + try: + return self.contract_body.text + except TimeoutException: + time.sleep(10) + pass + pytest.fail('Contract is not deployed in %s minutes!' % str(wait/60)) diff --git a/test/end-to-end/pages/thirdparty/metamask_plugin.py b/test/end-to-end/pages/thirdparty/metamask_plugin.py new file mode 100644 index 0000000..db11190 --- /dev/null +++ b/test/end-to-end/pages/thirdparty/metamask_plugin.py @@ -0,0 +1,88 @@ +import time +from pages.base_page import BasePageObject +from pages.base_element import * +from selenium.webdriver import ActionChains + + +class BasePluginButton(BaseButton): + + def click(self): + time.sleep(2) + self.find_element().click() + + +class AcceptButton(BasePluginButton): + + def __init__(self, driver): + super(AcceptButton, self).__init__(driver) + self.locator = self.Locator.xpath_selector("//button[.='Accept']") + + +class PrivacyText(BaseText): + + def __init__(self, driver): + super(PrivacyText, self).__init__(driver) + self.locator = self.Locator.xpath_selector("//a[.='Privacy']") + + +class ExportDenButton(BaseButton): + + def __init__(self, driver): + super(ExportDenButton, self).__init__(driver) + self.locator = self.Locator.xpath_selector("//p[.='Import Existing DEN']") + + +class SecretPhraseEditBox(BaseEditBox): + + def __init__(self, driver): + super(SecretPhraseEditBox, self).__init__(driver) + self.locator = self.Locator.xpath_selector("//textarea") + + +class PasswordEditBox(BaseEditBox): + + def __init__(self, driver): + super(PasswordEditBox, self).__init__(driver) + self.locator = self.Locator.id('password-box') + + +class PasswordConfirmEditBox(BaseEditBox): + + def __init__(self, driver): + super(PasswordConfirmEditBox, self).__init__(driver) + self.locator = self.Locator.id('password-box-confirm') + + +class OkButton(BasePluginButton): + + def __init__(self, driver): + super(OkButton, self).__init__(driver) + self.locator = self.Locator.xpath_selector("//button[.='OK']") + + +class MetaMaskPlugin(BasePageObject): + def __init__(self, driver): + super(MetaMaskPlugin, self).__init__(driver) + self.driver = driver + + self.accept_button = AcceptButton(self.driver) + self.privacy_text = PrivacyText(self.driver) + self.enter_secret_phrase = SecretPhraseEditBox(self.driver) + self.export_den_button = ExportDenButton(self.driver) + self.password_edit_box = PasswordEditBox(self.driver) + self.password_box_confirm = PasswordConfirmEditBox(self.driver) + self.ok_button = OkButton(self.driver) + + def recover_access(self, passphrase, password, confirm_password): + + self.get_url('chrome-extension://nkbihfbeogaeaoehlefnkodbefgpgknn/popup.html') + self.accept_button.click() + ActionChains(self.driver).move_to_element(self.privacy_text.find_element()).perform() + self.accept_button.click() + + self.export_den_button.click() + self.enter_secret_phrase.send_keys(passphrase) + self.password_edit_box.send_keys(password) + self.password_box_confirm.send_keys(confirm_password) + self.ok_button.click() + diff --git a/test/end-to-end/pytest.ini b/test/end-to-end/pytest.ini new file mode 100644 index 0000000..89c0c76 --- /dev/null +++ b/test/end-to-end/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +norecursedirs = .git pages +addopts = -s -v --junitxml=result.xml --tb=short diff --git a/test/end-to-end/requirements.txt b/test/end-to-end/requirements.txt new file mode 100644 index 0000000..397c508 --- /dev/null +++ b/test/end-to-end/requirements.txt @@ -0,0 +1,18 @@ +allpairspy==2.3.0 +apipkg==1.4 +certifi==2017.7.27.1 +chardet==3.0.4 +execnet==1.4.1 +idna==2.5 +lxml==3.8.0 +multidict==3.1.3 +namedlist==1.7 +py==1.4.34 +pytest==3.2.1 +pytest-forked==0.2 +pytest-xdist==1.20.0 +requests==2.18.3 +selenium==2.53.6 +six==1.10.0 +urllib3==1.22 +yarl==0.12.0 diff --git a/test/end-to-end/resources/metamask3_12_0.crx b/test/end-to-end/resources/metamask3_12_0.crx new file mode 100644 index 0000000..e7bda10 Binary files /dev/null and b/test/end-to-end/resources/metamask3_12_0.crx differ diff --git a/test/end-to-end/tests/__init__.py b/test/end-to-end/tests/__init__.py new file mode 100644 index 0000000..d9e141d --- /dev/null +++ b/test/end-to-end/tests/__init__.py @@ -0,0 +1,8 @@ + + +class TestData(object): + + def __init__(self): + self.test_name = None + +test_data = TestData() diff --git a/test/end-to-end/tests/basetestcase.py b/test/end-to-end/tests/basetestcase.py new file mode 100644 index 0000000..398673b --- /dev/null +++ b/test/end-to-end/tests/basetestcase.py @@ -0,0 +1,70 @@ +import pytest, sys +from selenium import webdriver +from selenium.common.exceptions import WebDriverException +from tests.postconditions import remove_application, remove_installation +from os import environ, path +from tests import test_data + + +class BaseTestCase: + + @property + def sauce_username(self): + return environ.get('SAUCE_USERNAME') + + + @property + def sauce_access_key(self): + return environ.get('SAUCE_ACCESS_KEY') + + + @property + def executor_sauce_lab(self): + return 'http://%s:%s@ondemand.saucelabs.com:80/wd/hub' % (self.sauce_username, self.sauce_access_key) + + def print_sauce_lab_info(self, driver): + sys.stdout = sys.stderr + print("SauceOnDemandSessionID=%s job-name=%s" % (driver.session_id, + pytest.config.getoption('build'))) + + @property + def capabilities_sauce_lab(self): + + desired_caps = dict() + desired_caps['name'] = test_data.test_name + desired_caps['build'] = pytest.config.getoption('build') + desired_caps['platform'] = "MAC" + desired_caps['browserName'] = 'Chrome' + desired_caps['screenResolution'] = '2048x1536' + desired_caps['captureHtml'] = False + return desired_caps + + def setup_method(self): + + self.errors = [] + + # options = webdriver.ChromeOptions() + # options.add_argument('--start-fullscreen') + # options.add_extension(path.abspath('/resources/metamask3_12_0.crx')) + + self.driver = webdriver.Remote(self.executor_sauce_lab, + desired_capabilities=self.capabilities_sauce_lab) + self.driver.implicitly_wait(5) + + def verify_no_errors(self): + if self.errors: + msg = '' + for error in self.errors: + msg += (error + '\n') + pytest.fail(msg, pytrace=False) + + def teardown_method(self): + + remove_application(self.driver) + remove_installation(self.driver) + + try: + self.print_sauce_lab_info(self.driver) + self.driver.quit() + except WebDriverException: + pass diff --git a/test/end-to-end/tests/conftest.py b/test/end-to-end/tests/conftest.py new file mode 100644 index 0000000..62f296e --- /dev/null +++ b/test/end-to-end/tests/conftest.py @@ -0,0 +1,23 @@ +from tests import test_data +from datetime import datetime + + +def pytest_addoption(parser): + parser.addoption("--build", + action="store", + default='SOB-' + datetime.now().strftime('%d-%b-%Y-%H-%M'), + help="Specify build name") + parser.addoption('--log', + action='store', + default=True, + help='Display each test step in terminal as plain text: True/False') + + +def pytest_configure(config): + if config.getoption('log'): + import logging + logging.basicConfig(level=logging.INFO) + + +def pytest_runtest_setup(item): + test_data.test_name = item.name diff --git a/test/end-to-end/tests/postconditions.py b/test/end-to-end/tests/postconditions.py new file mode 100644 index 0000000..b9f4c7d --- /dev/null +++ b/test/end-to-end/tests/postconditions.py @@ -0,0 +1,22 @@ +from selenium.webdriver.common.by import By +from selenium.common.exceptions import NoSuchElementException + + +def remove_application(driver): + try: + driver.get('https://github.com/settings/applications') + driver.find_element(By.CSS_SELECTOR, '.BtnGroup-item').click() + driver.find_element(By.CSS_SELECTOR, '.facebox-popup .btn-danger').click() + except NoSuchElementException: + pass + + +def remove_installation(driver): + try: + driver.get('https://github.com/organizations/Org4/settings/installations') + driver.find_element(By.CSS_SELECTOR, '.iconbutton').click() + driver.find_element(By.XPATH, "//a[@class='btn btn-danger']").click() + driver.find_element(By.CSS_SELECTOR, '.facebox-popup .btn-danger').click() + except NoSuchElementException: + pass + diff --git a/test/end-to-end/tests/test_contracts.py b/test/end-to-end/tests/test_contracts.py new file mode 100644 index 0000000..68bb4cb --- /dev/null +++ b/test/end-to-end/tests/test_contracts.py @@ -0,0 +1,22 @@ +import pytest +from os import environ +from pages.openbounty.landing import LandingPage +from tests.basetestcase import BaseTestCase + + +@pytest.mark.sanity +class TestLogin(BaseTestCase): + + def test_deploy_new_contract(self): + landing = LandingPage(self.driver) + landing.get_landing_page() + github = landing.login_button.click() + github.sign_in('anna04test', + 'f@E23D3H15Rd') + assert github.permission_type.text == 'Personal user data' + bounties_page = github.authorize_sob.click() + github.install_sob_plugin() + assert bounties_page.bounties_header.text == 'Bounties' + assert bounties_page.top_hunters_header.text == 'Top hunters' + github.create_new_bounty() + github.get_deployed_contract()