diff --git a/.circleci/config.yml b/.circleci/config.yml index d4726f1..c9eb25f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -44,5 +44,9 @@ jobs: - run: echo $ETH_WALLET_JSON > $ETH_WALLET_FILE - # run tests! - run: lein test + - run: lein uberjar + + - store_artifacts: + path: target/uberjar/commiteth.jar + destination: commiteth.jar diff --git a/Dockerfile b/Dockerfile index 83164d0..b920d3b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,6 +26,7 @@ WORKDIR /root/ RUN apt-get update RUN apt-get -y install xvfb RUN apt-get -y install wkhtmltopdf +RUN apt-get -y install less COPY --from=builder /usr/src/app/target/uberjar/commiteth.jar . COPY html2png.sh . diff --git a/README.md b/README.md index 66ddc01..2efc3ed 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ Key | Description --- | --- dev | Currently specifies whether Swagger UI endpoints should be added to routes port | HTTP port for the Ring web app +dev-login | Local development only. Set it to GitHub name of your dev user in order to login into the system bypassing OAuth. `server-address` has to be then correspondingly set to your localhost address. nrepl-port | nREPL port for development jdbc-database-url | PostgreSQL database URL. For instance, URL to local db would be `jdbc:postgresql://localhost/commiteth?user=commiteth&password=commiteth` server-address | URL and port of local server that can be resolved from public internet. It will be used as a redirect URI during GitHub OAuth authorization process diff --git a/doc/decisions/README.md b/doc/decisions/README.md index 0c501a7..6b229f9 100644 --- a/doc/decisions/README.md +++ b/doc/decisions/README.md @@ -1,6 +1,6 @@ # Decisions -We record decisions. More context on why and how we do that can be found in [DR-0001](doc/decisions/0001-record-decisions.md). +We record decisions. More context on why and how we do that can be found in [DR-0001](0001-record-decisions.md). The remainder of this document is intended to document tooling around this process. diff --git a/doc/testing.md b/doc/testing.md index 45404d7..20c61be 100644 --- a/doc/testing.md +++ b/doc/testing.md @@ -1,50 +1,62 @@ # Testing Open Bounty -We have a continuously deployed version tracking the `develop` branch live at https://openbounty.status.im:444. It uses the [Ropsten](https://ropsten.io/) Ethereum testnet. Any one is welcome to use it and your help with testing Open Bounty is greatly appreciated! +We have two environments for testing purposes: +* `staging` - where `develop` branch is continuously deployed -### General + **URL:** https://openbounty.status.im:444 + + **GitHub app:** https://github.com/apps/status-open-bounty-app-test + +* `testing`- where you can deploy and test separate pull requests + + **URL:** https://testing.openbounty.status.im + + **GitHub app:** https://github.com/apps/open-bounty-testing + +Both of them use the [Ropsten](https://ropsten.io/) Ethereum testnet. +Any one is welcome to use it and your help with testing Open Bounty is greatly appreciated! + + +## General For testing you will need: * a web browser (Chrome is known to work, testing with others appreciated) * an Ethereum account on the Ropsten testnet -* a Github account with administrative access to one or more repositories +* 2 Github accounts (one for Bounty Hunter, second - for Bounty Creator with administrative access to one or more repositories) * for approving bounty payouts you will additionally need access to an Ethereum wallet. ([MetaMask](https://metamask.io/) plugin) The developers can be reached on the `#openbounty` channel in the [Status slack](http://slack.status.im/). - ### Signing up -* point your browser to https://openbounty.status.im:444 and click `Login` +Process is the same for Bounty Creator and Bounty Hunter. + +* point your browser to URL of selected environment (`staging` or `testing`) and click `Login` * Authorise status-open-bounty to have read access to your public GitHub profile. -You should now see `Bounties`, `Activity`, `Repositories` and `Manage Payouts` tabs. In the upper right hand corner, there should be a dropdown with your GitHub username and options `My Payment Details` and `Sign Out`. +You should now see `Bounties`, `Activity`, `Repositories` tabs. In the upper right hand corner, there should be a dropdown with your GitHub username and options `My Payment Details` and `Sign Out`. +## For Bounty Creator ### Connecting your wallet -(instructions for Metamask) * install Metamask and configure your account * select `My Payment Details` from the top-right dropdown, select the account you want to use from the selection list and click `Update` ### Creating bounty issues -Before you can create bounties, you need to add Open Bounty GitHub App to your account or repos. Go to https://github.com/apps/status-open-bounty-app-test (or link to another GitHub App you've created for testing, as described in the [README](README.md) and click Install. Specify whether access to all org repos or specific repos is granted. This will install webhooks for SOB in your repos. +Before you can create bounties, you need to add Open Bounty GitHub App to your account or repos. Go to **GitHub app** for selected environment(or link to another GitHub App you've created for testing, as described in the [README](README.md) and click Install. Specify whether access to all org repos or specific repos is granted. This will install webhooks for SOB in your repos. * Request for your account to be whitelisted. Contact [Riot](https://chat.status.im) for more information * now, add the `bounty` label to a new or an existing issue. This should cause Status Open Bounty to post a new comment for the issue containing an image with text `Deploying contract, please wait` * once the contract has been mined, the comment will be updated to contain the bounty contract's address and a QR code +* in SOB `Manage Payouts` should appear (when you logged in) ### Funding bounties The Github comment has a QR code as an image containing the bounty contract address. The address is also on the comment as text. Use any ethereum wallet to send ETH and/or supported ERC20 tokens to this address. After a small delay (max 5 minutes), the activity feed should show that the related bounty issue's balance increased and comment should be updated. - -### Submitting claims - -To get bounties you need to provide an Ethereum address in you Payment details on the https://openbounty.status.im:444 that will be used to send bounties to. - -Open a pull request against the target repository with `Fixes: #NN` in the comment where `NN` is the issue number of the bountied Github issue. After the PR has been opened, the activity feed should show an item indicating that your username has opened a claim for the related bounty issue. The repository admin should also see the claim under `Open claims` in the `Manage payouts` view. +After this process bounty is available for BountyHunters on URL of selected environment in `Open Bounty` list. ### Managing payouts @@ -52,8 +64,20 @@ Repository admins see a listing of all open claims and bounties that have alread ### Removing bounties -To remove issue from the Bounties list you can close it in GitHub. +To remove issue from the Bounties list you can close it in GitHub. +## For Bounty Hunter + +### Submitting claims + +Whole process is explained here: [Status Open Bounty Tutorial](https://www.youtube.com/watch?v=vTjcXP4kTHc). + +To get bounties you need to provide an Ethereum address in you Payment details on the selected that will be used to send bounties to. + +Open a pull request against the target repository with any keyword from [Closing issues using keywords](https://help.github.com/articles/closing-issues-using-keywords/), i. e. `Fixes: #NN` in the comment where `NN` is the issue number of the bountied Github issue. +After the PR has been opened, the `Activity` feed should show an item indicating that your username has opened a claim for the related bounty issue. The repository admin (Bounty Creator) should also see the claim under `Open claims` in the `Manage payouts` view. + +Flow walkthrough for Bounty Creator and Bounty Contributor is [here](https://docs.google.com/presentation/d/1btWVeaqR6yPLSHHZQ2XgfgK8MU2tWuAtNUE_5hdfJCI/edit#slide=id.g314ca9a4e1_0_0). ### Reporting bugs All bugs should be reported as issues in the [OpenBounty Github repository](https://github.com/status-im/open-bounty/issues). diff --git a/resources/migrations/20180403131200-multiple-claims-in-pr.down.sql b/resources/migrations/20180403131200-multiple-claims-in-pr.down.sql new file mode 100644 index 0000000..096ced8 --- /dev/null +++ b/resources/migrations/20180403131200-multiple-claims-in-pr.down.sql @@ -0,0 +1,7 @@ +ALTER TABLE pull_requests DROP CONSTRAINT pull_requests_pkey; +ALTER TABLE pull_requests DROP CONSTRAINT pull_requests_pr_id_key; +ALTER TABLE pull_requests DROP CONSTRAINT pull_requests_fkey; + + +ALTER TABLE pull_requests ADD CONSTRAINT pull_requests_pkey PRIMARY KEY (pr_id); +ALTER TABLE pull_requests ADD CONSTRAINT pull_requests_pr_id_key UNIQUE (pr_id); diff --git a/resources/migrations/20180403131200-multiple-claims-in-pr.up.sql b/resources/migrations/20180403131200-multiple-claims-in-pr.up.sql new file mode 100644 index 0000000..c7b29fb --- /dev/null +++ b/resources/migrations/20180403131200-multiple-claims-in-pr.up.sql @@ -0,0 +1,6 @@ +ALTER TABLE pull_requests DROP CONSTRAINT pull_requests_pkey; +ALTER TABLE pull_requests DROP CONSTRAINT pull_requests_pr_id_key; + +ALTER TABLE pull_requests ADD CONSTRAINT pull_requests_pkey PRIMARY KEY (pr_id, issue_id); +ALTER TABLE pull_requests ADD CONSTRAINT pull_requests_pr_id_key UNIQUE (pr_id, issue_id); +ALTER TABLE pull_requests ADD CONSTRAINT pull_requests_fkey FOREIGN KEY (issue_id) REFERENCES issues(issue_id); diff --git a/resources/sql/queries.sql b/resources/sql/queries.sql index 4f866af..4c061f8 100644 --- a/resources/sql/queries.sql +++ b/resources/sql/queries.sql @@ -43,6 +43,13 @@ SELECT * FROM users WHERE id = :id; +-- :name get-user-by-login :? :1 +-- :doc retrieve a user given GitHub login. +SELECT * +FROM users +WHERE login = :login; + + -- :name get-repo-owner :? :1 SELECT * FROM users u, repositories r @@ -213,7 +220,7 @@ VALUES(:pr_id, :commit_sha, :user_id, :state) -ON CONFLICT (pr_id) DO UPDATE +ON CONFLICT (pr_id,issue_id) DO UPDATE SET state = :state, issue_number = :issue_number, diff --git a/src/clj/commiteth/bounties.clj b/src/clj/commiteth/bounties.clj index 13661ae..2be8511 100644 --- a/src/clj/commiteth/bounties.clj +++ b/src/clj/commiteth/bounties.clj @@ -22,24 +22,22 @@ (defn deploy-contract [owner owner-address repo issue-id issue-number] (if (empty? owner-address) - (log/error "Unable to deploy bounty contract because" - "repo owner has no Ethereum addres") + (log/errorf "issue %s: Unable to deploy bounty contract because repo owner has no Ethereum addres" issue-id) (do - (log/info "deploying contract to " owner-address) + (log/infof "issue %s: Deploying contract to %s" issue-id owner-address) (if-let [transaction-hash (multisig/deploy-multisig owner-address)] (do - (log/info "Contract deployed, transaction-hash:" - transaction-hash) + (log/infof "issue %s: Contract deployed, transaction-hash: %s" issue-id transaction-hash) (let [resp (github/post-deploying-comment owner repo issue-number transaction-hash) - _ (log/info "post-deploying-comment response:" resp) comment-id (:id resp)] + (log/infof "issue %s: post-deploying-comment response: %s" issue-id resp) (issues/update-comment-id issue-id comment-id)) (issues/update-transaction-hash issue-id transaction-hash)) - (log/error "Failed to deploy contract to" owner-address))))) - + (log/errorf "issue %s Failed to deploy contract to %s" issue-id owner-address))))) + (defn add-bounty-for-issue [repo repo-id issue] (let [{issue-id :id issue-number :number @@ -47,16 +45,17 @@ created-issue (issues/create repo-id issue-id issue-number issue-title) {owner-address :address owner :owner} (users/get-repo-owner repo-id)] - (log/debug "Adding bounty for issue " repo issue-number "owner address: " owner-address) + (log/debug "issue %s: Adding bounty for issue %s/%s - owner address: %s" + issue-id repo issue-number owner-address) (if (= 1 created-issue) (deploy-contract owner owner-address repo issue-id issue-number) - (log/debug "Issue already exists in DB, ignoring")))) + (log/debug "issue %s: Issue already exists in DB, ignoring")))) (defn maybe-add-bounty-for-issue [repo repo-id issue] (let [res (issues/get-issues-count repo-id) {count :count} res - limit-reached? (> count max-issues-limit) - _ (log/debug "*** get-issues-count" repo-id " " res " " count " " limit-reached?)] + limit-reached? (> count max-issues-limit)] + (log/debug "*** get-issues-count" repo-id " " res " " count " " limit-reached?) (if limit-reached? (log/debug "Total issues for repo limit reached " repo " " count) (add-bounty-for-issue repo repo-id issue)))) @@ -102,9 +101,7 @@ (issues/get-issue-titles)] (let [gh-issue (github/get-issue owner repo issue_number)] (if-not (= title (:title gh-issue)) - (do - (log/info "Updating changed title for issue" (:id gh-issue)) - (issues/update-issue-title (:id gh-issue) (:title gh-issue))))))) + (issues/update-issue-title (:id gh-issue) (:title gh-issue)))))) (defn assert-keys [m ks] (doseq [k ks] diff --git a/src/clj/commiteth/db/issues.clj b/src/clj/commiteth/db/issues.clj index fe4b6ce..d910aae 100644 --- a/src/clj/commiteth/db/issues.clj +++ b/src/clj/commiteth/db/issues.clj @@ -1,7 +1,8 @@ (ns commiteth.db.issues (:require [commiteth.db.core :refer [*db*] :as db] [clojure.java.jdbc :as jdbc] - [clojure.set :refer [rename-keys]])) + [clojure.set :refer [rename-keys]] + [clojure.tools.logging :as log])) (defn create "Creates issue" @@ -31,6 +32,7 @@ (defn update-issue-title [issue-id title] + (log/info "issue %s: Updating changed title \"%s\"" issue-id title) (jdbc/with-db-connection [con-db *db*] (db/update-issue-title con-db {:issue_id issue-id :title title}))) diff --git a/src/clj/commiteth/db/users.clj b/src/clj/commiteth/db/users.clj index 488a5db..20b4ff8 100644 --- a/src/clj/commiteth/db/users.clj +++ b/src/clj/commiteth/db/users.clj @@ -21,6 +21,11 @@ (jdbc/with-db-connection [con-db *db*] (db/get-user con-db {:id user-id}))) +(defn get-user-by-login + [login] + (jdbc/with-db-connection [con-db *db*] + (db/get-user-by-login con-db {:login login}))) + (defn exists? [user-id] (jdbc/with-db-connection [con-db *db*] diff --git a/src/clj/commiteth/eth/core.clj b/src/clj/commiteth/eth/core.clj index 8f45808..4db60c1 100644 --- a/src/clj/commiteth/eth/core.clj +++ b/src/clj/commiteth/eth/core.clj @@ -74,11 +74,19 @@ signed (TransactionEncoder/signMessage tx (creds)) hex-string (Numeric/toHexString signed)] hex-string)) + (defn eth-gasstation-gas-price + "Get max of average and average_calc from gas station and use that + as gas price. average_calc is computed from a larger time period than average, + so the idea is to account for both temporary dips (when average_calc > average) + and temporary rises (average_calc < average) of gas price" [] (let [data (json-api-request "https://ethgasstation.info/json/ethgasAPI.json") - avg-price (-> (get data "average") - bigint) + avg-price (max + (-> (get data "average") + bigint) + (-> (get data "average_calc") + bigint)) avg-price-gwei (/ avg-price (bigint 10))] (->> (* (bigint (Math/pow 10 9)) avg-price-gwei) ;; for some reason the API returns 10x gwei price .toBigInteger))) diff --git a/src/clj/commiteth/routes/home.clj b/src/clj/commiteth/routes/home.clj index 1230d13..1702369 100644 --- a/src/clj/commiteth/routes/home.clj +++ b/src/clj/commiteth/routes/home.clj @@ -23,7 +23,10 @@ :on-testnet? (env :on-testnet)})) (defn landing-page [] - (layout/render "index.html" {:authorize-url (github/signup-authorize-url)})) + (layout/render "index.html" + {:authorize-url (if (env :dev-login) + (str (env :server-address) "/callback_dev") + (github/signup-authorize-url))})) (defn welcome-page [] (layout/render "welcome.html")) diff --git a/src/clj/commiteth/routes/redirect.clj b/src/clj/commiteth/routes/redirect.clj index 6aaaf2e..654d5fa 100644 --- a/src/clj/commiteth/routes/redirect.clj +++ b/src/clj/commiteth/routes/redirect.clj @@ -35,28 +35,33 @@ (defroutes redirect-routes (GET "/callback" [code state] - (let [resp (github/post-for-token code state) - body (keywordize-keys (codec/form-decode (:body resp))) - scope (:scope body) - access-token (:access_token body)] - (log/info "access-token:" access-token) - (log/debug "github sign-in callback, response body:" body) - (if (:error body) - ;; Why does Mist browser sends two redirects at the same time? The latter results in 401 error. - (found (str (env :server-address) "/app")) - (let [admin-token? (str/includes? scope "repo") - token-key (if admin-token? :admin-token :token) - gh-user (github/get-user access-token) - new-user? (nil? (users/get-user (:id gh-user 0))) - user (assoc (get-or-create-user access-token) - token-key access-token)] - (when (and (hubspot-contact-create-enabled) - new-user?) - (try - (hubspot/create-hubspot-contact (:email user) - (:name user "") - (:login user)) - (catch Throwable t - (log/error "Failed to create hubspot contact" t)))) - (assoc (found (str (env :server-address) "/app")) - :session {:identity user})))))) + (let [resp (github/post-for-token code state) + body (keywordize-keys (codec/form-decode (:body resp))) + scope (:scope body) + access-token (:access_token body)] + (log/info "access-token:" access-token) + (log/debug "github sign-in callback, response body:" body) + (if (:error body) + ;; Why does Mist browser sends two redirects at the same time? The latter results in 401 error. + (found (str (env :server-address) "/app")) + (let [admin-token? (str/includes? scope "repo") + token-key (if admin-token? :admin-token :token) + gh-user (github/get-user access-token) + new-user? (nil? (users/get-user (:id gh-user 0))) + user (assoc (get-or-create-user access-token) + token-key access-token)] + (when (and (hubspot-contact-create-enabled) + new-user?) + (try + (hubspot/create-hubspot-contact (:email user) + (:name user "") + (:login user)) + (catch Throwable t + (log/error "Failed to create hubspot contact" t)))) + (assoc (found (str (env :server-address) "/app")) + :session {:identity user}))))) + (GET "/callback_dev" [] + (assoc (found (str (env :server-address) "/app")) + :session {:identity (users/get-user-by-login (env :dev-login))})) + + ) diff --git a/src/clj/commiteth/routes/webhooks.clj b/src/clj/commiteth/routes/webhooks.clj index 3a7e44f..6c2f1e7 100644 --- a/src/clj/commiteth/routes/webhooks.clj +++ b/src/clj/commiteth/routes/webhooks.clj @@ -174,24 +174,23 @@ pr-body :body pr-title :title} :pull_request}] (log/info "handle-pull-request-event" event-type owner repo repo-id login pr-body pr-title) - (if-let [issue (some->> (extract-issue-number owner repo pr-body pr-title) - (first) - (issues/get-issue repo-id))] - (if-not (:commit_sha issue) ; no PR has been merged yet referencing this issue - (do - (log/info "Referenced bounty issue found" owner repo (:issue_number issue)) - (handle-claim issue - user-id - login name - avatar_url - owner repo - repo-id - pr-id - pr-number - head-sha - merged? - event-type)) - (log/info "PR for issue already merged")) + (if-let [issues (remove nil? (map #(issues/get-issue repo-id %1) (extract-issue-number owner repo pr-body pr-title)))] + (doseq [issue issues] + (if-not (:commit_sha issue) ; no PR has been merged yet referencing this issue + (do + (log/info "Referenced bounty issue found" owner repo (:issue_number issue)) + (handle-claim issue + user-id + login name + avatar_url + owner repo + repo-id + pr-id + pr-number + head-sha + merged? + event-type)) + (log/info "PR for issue already merged"))) (when (= :edited event-type) ; Remove PR if it does not reference any issue (pull-requests/remove pr-id)))) diff --git a/src/clj/commiteth/scheduler.clj b/src/clj/commiteth/scheduler.clj index 8400033..907b143 100644 --- a/src/clj/commiteth/scheduler.clj +++ b/src/clj/commiteth/scheduler.clj @@ -38,6 +38,7 @@ (update-balances))) ) + (defn update-issue-contract-address "For each pending deployment: gets transaction receipt, updates db state (contract-address, comment-id) and posts github comment" @@ -45,56 +46,57 @@ (log/info "In update-issue-contract-address") (p :update-issue-contract-address (doseq [{issue-id :issue_id - transaction-hash :transaction_hash} (issues/list-pending-deployments)] - (log/info "pending deployment:" transaction-hash) - (try - (when-let [receipt (eth/get-transaction-receipt transaction-hash)] - (log/info "update-issue-contract-address: transaction receipt for issue #" - issue-id ": " receipt) - (if-let [contract-address (multisig/find-created-multisig-address receipt)] - (let [issue (issues/update-contract-address issue-id contract-address) - {owner :owner - repo :repo - comment-id :comment_id - issue-number :issue_number} issue - balance-eth-str (eth/get-balance-eth contract-address 6) - balance-eth (read-string balance-eth-str)] - (log/info "Updating comment image") - (bounties/update-bounty-comment-image issue-id - owner - repo - issue-number - contract-address - balance-eth - balance-eth-str - {}) - (log/info "Updating comment") - (github/update-comment owner - repo - comment-id - issue-number - contract-address - balance-eth - balance-eth-str - {})) - (log/error "Failed to find contract address in tx logs"))) - (catch Throwable ex - (do (log/error "update-issue-contract-address exception:" ex) - (clojure.stacktrace/print-stack-trace ex)))))) + transaction-hash :transaction_hash} (issues/list-pending-deployments)] + (log/infof "issue %s: pending deployment: %s" issue-id transaction-hash) + (try + (when-let [receipt (eth/get-transaction-receipt transaction-hash)] + (log/infof "issue %s: update-issue-contract-address: tx receipt: %s" issue-id receipt) + (if-let [contract-address (multisig/find-created-multisig-address receipt)] + (let [issue (issues/update-contract-address issue-id contract-address) + {owner :owner + repo :repo + comment-id :comment_id + issue-number :issue_number} issue + balance-eth-str (eth/get-balance-eth contract-address 6) + balance-eth (read-string balance-eth-str)] + (log/infof "issue %s: Updating comment image" issue-id) + (bounties/update-bounty-comment-image issue-id + owner + repo + issue-number + contract-address + balance-eth + balance-eth-str + {}) + (log/infof "issue %s: Updating comment" issue-id) + (github/update-comment owner + repo + comment-id + issue-number + contract-address + balance-eth + balance-eth-str + {})) + (log/errorf "issue %s: Failed to find contract address in tx logs" issue-id))) + (catch Throwable ex + (log/errorf ex "issue %s: update-issue-contract-address exception:" issue-id))))) (log/info "Exit update-issue-contract-address")) (defn deploy-pending-contracts "Under high-concurrency circumstances or in case geth is in defunct state, a bounty contract may not deploy successfully when the bounty label is addded to an issue. This function deploys such contracts." [] - (p :deploy-pending-contracts - (doseq [{issue-id :issue_id - issue-number :issue_number - owner :owner - owner-address :owner_address - repo :repo} (db-bounties/pending-contracts)] - (log/debug "Trying to re-deploy failed bounty contract deployment, issue-id:" issue-id) - (bounties/deploy-contract owner owner-address repo issue-id issue-number)))) + (p :deploy-pending-contracts + (doseq [{issue-id :issue_id + issue-number :issue_number + owner :owner + owner-address :owner_address + repo :repo} (db-bounties/pending-contracts)] + (log/infof "issue %s: Trying to re-deploy failed bounty contract deployment" issue-id) + (try + (bounties/deploy-contract owner owner-address repo issue-id issue-number) + (catch Throwable ex + (log/errorf ex "issue %s: deploy-pending-contracts exception:" issue-id)))))) (defn self-sign-bounty "Walks through all issues eligible for bounty payout and signs corresponding transaction" @@ -110,37 +112,35 @@ issue-number :issue_number balance-eth :balance_eth tokens :tokens - winner-login :winner_login} (db-bounties/pending-bounties)] - (try - (let [value (eth/get-balance-hex contract-address)] - (if (empty? payout-address) - (do - (log/error "Cannot sign pending bounty - winner has no payout address") - (github/update-merged-issue-comment owner - repo - comment-id - contract-address - (eth-decimal->str balance-eth) - tokens - winner-login - true)) - (let [execute-hash (multisig/send-all contract-address payout-address)] - (log/info "Payout self-signed, called sign-all(" contract-address payout-address ") tx:" execute-hash) - (db-bounties/update-execute-hash issue-id execute-hash) - (db-bounties/update-winner-login issue-id winner-login) - (github/update-merged-issue-comment owner - repo - comment-id - contract-address - (eth-decimal->str balance-eth) - tokens - winner-login - false)))) - (catch Throwable ex - (do (log/error "self-sign-bounty exception:" ex) - (clojure.stacktrace/print-stack-trace ex)))))) - (log/info "Exit self-sign-bounty") - ) + winner-login :winner_login} (db-bounties/pending-bounties)] + (try + (let [value (eth/get-balance-hex contract-address)] + (if (empty? payout-address) + (do + (log/warn "issue %s: Cannot sign pending bounty - winner has no payout address" issue-id) + (github/update-merged-issue-comment owner + repo + comment-id + contract-address + (eth-decimal->str balance-eth) + tokens + winner-login + true)) + (let [execute-hash (multisig/send-all contract-address payout-address)] + (log/infof "issue %s: Payout self-signed, called sign-all(%s) tx: %s" issue-id contract-address payout-address execute-hash) + (db-bounties/update-execute-hash issue-id execute-hash) + (db-bounties/update-winner-login issue-id winner-login) + (github/update-merged-issue-comment owner + repo + comment-id + contract-address + (eth-decimal->str balance-eth) + tokens + winner-login + false)))) + (catch Throwable ex + (log/error ex "issue %s: self-sign-bounty exception" issue-id))))) + (log/info "Exit self-sign-bounty")) (defn update-confirm-hash "Gets transaction receipt for each pending payout and updates DB confirm_hash with tranaction ID of commiteth bot account's confirmation." @@ -148,13 +148,16 @@ (log/info "In update-confirm-hash") (p :update-confirm-hash (doseq [{issue-id :issue_id - execute-hash :execute_hash} (db-bounties/pending-payouts)] - (log/info "pending payout:" execute-hash) - (when-let [receipt (eth/get-transaction-receipt execute-hash)] - (log/info "execution receipt for issue #" issue-id ": " receipt) - (when-let [confirm-hash (multisig/find-confirmation-tx-id receipt)] - (log/info "confirm hash:" confirm-hash) - (db-bounties/update-confirm-hash issue-id confirm-hash))))) + execute-hash :execute_hash} (db-bounties/pending-payouts)] + (log/infof "issue %s: pending payout: %s" issue-id execute-hash) + (try + (when-let [receipt (eth/get-transaction-receipt execute-hash)] + (log/infof "issue %s: execution receipt for issue " issue-id receipt) + (when-let [confirm-hash (multisig/find-confirmation-tx-id receipt)] + (log/infof "issue %s: confirm hash:" issue-id confirm-hash) + (db-bounties/update-confirm-hash issue-id confirm-hash))) + (catch Throwable ex + (log/errorf ex "issue %s: update-confirm-hash exception:" issue-id))))) (log/info "Exit update-confirm-hash")) @@ -163,10 +166,13 @@ [] (p :update-watch-hash (doseq [{issue-id :issue_id - watch-hash :watch_hash} (db-bounties/pending-watch-calls)] - (log/info "pending watch call" watch-hash) - (when-let [receipt (eth/get-transaction-receipt watch-hash)] - (db-bounties/update-watch-hash issue-id nil))))) + watch-hash :watch_hash} (db-bounties/pending-watch-calls)] + (log/infof "issue %s: pending watch call %s" issue-id watch-hash) + (try + (when-let [receipt (eth/get-transaction-receipt watch-hash)] + (db-bounties/update-watch-hash issue-id nil)) + (catch Throwable ex + (log/errorf ex "issue %s: update-watch-hash exception:" issue-id)))))) (defn older-than-3h? @@ -194,7 +200,7 @@ confirm-id :confirm_hash payee-login :payee_login updated :updated} (db-bounties/confirmed-payouts)] - (log/debug "confirmed payout:" payout-hash) + (log/infof "issue %s: confirmed payout: %s" issue-id payout-hash) (try (if-let [receipt (eth/get-transaction-receipt payout-hash)] (let [contract-tokens (multisig/token-balances contract-address) @@ -203,14 +209,14 @@ (some #(> (second %) 0.0) contract-tokens) (> contract-eth-balance 0)) (do - (log/info "Contract still has funds") + (log/infof "issue %s: Contract (%s) still has funds" issue-id contract-address) (when (multisig/is-confirmed? contract-address confirm-id) - (log/info "Detected bounty with funds and confirmed payout, calling executeTransaction") + (log/infof "issue %s: Detected bounty with funds and confirmed payout, calling executeTransaction" issue-id) (let [execute-tx-hash (multisig/execute-tx contract-address confirm-id)] - (log/info "execute tx:" execute-tx-hash)))) + (log/infof "issue %s: execute tx: %s" issue-id execute-tx-hash)))) (do - (log/info "Payout has succeeded, saving payout receipt for issue #" issue-id ": " receipt) + (log/infof "issue %s: Payout has succeeded, payout receipt %s" issue-id receipt) (db-bounties/update-payout-receipt issue-id receipt) (github/update-paid-issue-comment owner repo @@ -220,13 +226,11 @@ tokens payee-login)))) (when (older-than-3h? updated) - (log/info "Resetting payout hash for issue" issue-id "as it has not been mined in 3h") + (log/warn "issue %s: Resetting payout hash for issue as it has not been mined in 3h" issue-id) (db-bounties/reset-payout-hash issue-id))) - (catch Throwable ex - (do (log/error "update-payout-receipt exception:" ex) - (clojure.stacktrace/print-stack-trace ex)))))) - (log/info "Exit update-payout-receipt") - ) + (catch Throwable ex + (log/error ex "issue %s: update-payout-receipt exception" issue-id))))) + (log/info "Exit update-payout-receipt")) (defn abs "(abs n) is the absolute value of n" @@ -239,26 +243,27 @@ (defn update-bounty-token-balances - "Helper function for updating internal ERC20 token balances to token multisig contract. Will be called periodically for all open bounty contracts." + "Helper function for updating internal ERC20 token balances to token + multisig contract. Will be called periodically for all open bounty + contracts." [issue-id bounty-addr watch-hash] - #_(log/info "In update-bounty-token-balances for issue" issue-id) + (log/info "In update-bounty-token-balances for issue" issue-id) (doseq [[tla token-data] (token-data/as-map)] (try (let [balance (multisig/token-balance bounty-addr tla)] (when (> balance 0) (do - (log/info "bounty at" bounty-addr "has" balance "of token" tla) + (log/infof "bounty %s: has %s of token %s" bounty-addr balance tla) (let [internal-balance (multisig/token-balance-in-bounty bounty-addr tla)] (when (and (nil? watch-hash) (not= balance internal-balance)) - (log/info "balances not in sync, calling watch") + (log/infof "bounty %s: balances not in sync, calling watch" bounty-addr) (let [hash (multisig/watch-token bounty-addr tla)] (db-bounties/update-watch-hash issue-id hash))))))) - (catch Throwable ex - (do (log/error "update-bounty-token-balances exception:" ex) - (clojure.stacktrace/print-stack-trace ex))))) - #_(log/info "Exit update-bounty-token-balances")) - + (catch Throwable ex + (log/error ex "bounty %s: update-bounty-token-balances exception" bounty-addr)))) + (log/info "Exit update-bounty-token-balances")) + (defn update-contract-internal-balances "It is required in our current smart contract to manually update it's internal balance when some tokens have been added." @@ -296,8 +301,8 @@ [] (p :update-open-issue-usd-values (doseq [{bounty-addr :contract_address} - (db-bounties/open-bounty-contracts)] - (update-issue-usd-value bounty-addr)))) + (db-bounties/open-bounty-contracts)] + (update-issue-usd-value bounty-addr)))) (defn float= ([x y] (float= x y 0.0000001)) @@ -327,7 +332,7 @@ (let [balance-eth-str (eth/get-balance-eth contract-address 6) balance-eth (read-string balance-eth-str) token-balances (multisig/token-balances contract-address)] - (log/debug "update-balances" balance-eth + (log/debug "issue" issue-id ": update-balances" balance-eth balance-eth-str token-balances owner repo issue-number) (when (or @@ -361,8 +366,7 @@ token-balances) (update-issue-usd-value contract-address)))) (catch Throwable ex - (do (log/error "update-balances exception:" ex) - (clojure.stacktrace/print-stack-trace ex)))))) + (log/error ex "issue %s: update-balances exception" issue-id))))) (log/info "Exit update-balances")) @@ -370,7 +374,7 @@ (try (func) (catch Throwable t - (log/error t)))) + (log/error t (.getMessage t) (ex-data t))))) (defn run-tasks [tasks] (doall diff --git a/src/cljs/commiteth/handlers.cljs b/src/cljs/commiteth/handlers.cljs index 0f436dc..61447c1 100644 --- a/src/cljs/commiteth/handlers.cljs +++ b/src/cljs/commiteth/handlers.cljs @@ -13,7 +13,8 @@ :as rf-storage :refer [reg-co-fx!]] [commiteth.ui-model :as ui-model] - [commiteth.common :as common])) + [commiteth.common :as common] + [commiteth.routes :as routes])) (rf-storage/reg-co-fx! :commiteth-sob {:fx :store @@ -40,6 +41,20 @@ (println "redirecting to" path) (set! (.-pathname js/location) path))) +(reg-fx + :persist-bounty-filters-in-query + (fn [{:keys [bounty-filters]}] + (let [query + (->> bounty-filters + (remove (comp nil? val)) + (map (fn [[k v]] + [(ui-model/bounty-filter-type->query-param k) + (ui-model/bounty-filter-value->query-param k v)])) + (into {}))] + (routes/nav! :bounties {} (if (= {} query) + nil + query))))) + (reg-event-fx :initialize-db [(inject-cofx :store)] @@ -65,10 +80,17 @@ (reg-event-db :set-active-page - (fn [db [_ page]] + (fn [db [_ page params query]] (assoc db :page page :page-number 1 - ::db/open-bounties-filters {} + ::db/open-bounties-filters + (reduce-kv + #(let [type (ui-model/query-param->bounty-filter-type %2)] + (assoc %1 + type + (ui-model/query-param->bounty-filter-value type %3))) + {} + query) ::db/open-bounties-sorting-type ::ui-model/bounty-sorting-type|most-recent))) (reg-event-db @@ -468,9 +490,12 @@ (merge db {::db/open-bounties-sorting-type sorting-type :page-number 1}))) -(reg-event-db - ::set-open-bounty-filter-type - (fn [db [_ filter-type filter-value]] - (-> db - (assoc-in [::db/open-bounties-filters filter-type] filter-value) - (assoc :page-number 1)))) +(reg-event-fx + ::set-open-bounty-filter-type + (fn [{:keys [event db]} [_ filter-type filter-value]] + (println "db" db) + (let [filters (::db/open-bounties-filters db)] + (println "filters" filters) + {:persist-bounty-filters-in-query + {:bounty-filters + (assoc filters filter-type filter-value)}}))) diff --git a/src/cljs/commiteth/routes.cljs b/src/cljs/commiteth/routes.cljs index 1c3cc5e..c5cb2f5 100644 --- a/src/cljs/commiteth/routes.cljs +++ b/src/cljs/commiteth/routes.cljs @@ -14,12 +14,17 @@ "A function which will be called on each route change." [name params query] (println "Route change to: " name params query) - (rf/dispatch [:set-active-page name])) + (rf/dispatch [:set-active-page name params query])) (defn setup-nav! [] (bide/start! router {:default :bounties :on-navigate on-navigate})) -(defn nav! [route-id] - (bide/navigate! router route-id {})) +(defn nav! + ([route-id] + (nav! route-id nil)) + ([route-id params] + (nav! route-id params nil)) + ([route-id params query] + (bide/navigate! router route-id params query))) diff --git a/src/cljs/commiteth/subscriptions.cljs b/src/cljs/commiteth/subscriptions.cljs index a48ee67..3ecd6e2 100644 --- a/src/cljs/commiteth/subscriptions.cljs +++ b/src/cljs/commiteth/subscriptions.cljs @@ -179,7 +179,7 @@ (mapcat keys) (filter identity) set)] - (into #{"ETH"} token-ids)))) + (into #{:ETH} token-ids)))) (reg-sub ::filtered-and-sorted-open-bounties diff --git a/src/cljs/commiteth/ui_model.cljs b/src/cljs/commiteth/ui_model.cljs index d814a45..d953264 100644 --- a/src/cljs/commiteth/ui_model.cljs +++ b/src/cljs/commiteth/ui_model.cljs @@ -87,11 +87,11 @@ ::bounty-filter-type.category ::bounty-filter-type-category|multiple-dynamic-options ::bounty-filter-type.re-frame-subs-key-for-options :commiteth.subscriptions/open-bounties-currencies ::bounty-filter-type.predicate (fn [filter-value bounty] - (or (and (contains? filter-value "ETH") + (or (and (contains? filter-value :ETH) (< 0 (js/parseFloat (:balance-eth bounty)))) (not-empty (set/intersection - (->> filter-value (remove #{"ETH"}) set) + (->> filter-value (remove #{:ETH}) set) (-> bounty :tokens keys set)))))} ::bounty-filter-type|date @@ -124,6 +124,50 @@ (defn bounty-filter-type->name [filter-type] (-> bounty-filter-types-def (get filter-type) ::bounty-filter-type.name)) +(defn bounty-filter-type->query-param [filter-type] + (-> filter-type + name + (clojure.string/replace #".*\|" ""))) + +(defn bounty-filter-value->query-param [type value] + (let [category (-> bounty-filter-types-def type ::bounty-filter-type.category)] + (cond + (= category ::bounty-filter-type-category|multiple-dynamic-options) + (vec value) + + (= category ::bounty-filter-type-category|single-static-option) + (bounty-filter-type->query-param value) + + (= category ::bounty-filter-type-category|range) + (str (first value) "-" (second value))))) + +(defn query-param->bounty-filter-type [query-param] + (keyword (namespace ::_) (str "bounty-filter-type|" (name query-param)))) + +(defn query-param->bounty-filter-value [type value] + (let [category (-> bounty-filter-types-def type ::bounty-filter-type.category)] + (cond + (= type ::bounty-filter-type|currency) + (if (string? value) + #{(keyword value)} + (set (map keyword value))) + + (= type ::bounty-filter-type|owner) + (if (string? value) + #{value} + (set value)) + + (= type ::bounty-filter-type|claims) + (keyword (namespace ::_) + (str "bounty-filter-type-claims-option|" (name value))) + + (= type ::bounty-filter-type|date) + (keyword (namespace ::_) + (str "bounty-filter-type-date-option|" (name value))) + + (= category ::bounty-filter-type-category|range) + (clojure.string/split value #"-")))) + (defn bounty-filter-value->short-text [filter-type filter-value] (cond (= filter-type ::bounty-filter-type|date) diff --git a/test/Jenkinsfile b/test/Jenkinsfile index 16ee5e4..b6e92ea 100644 --- a/test/Jenkinsfile +++ b/test/Jenkinsfile @@ -2,11 +2,11 @@ node ('linux1') {sauce('1be1b688-e0e7-4314-92a0-db11f52d3c00') { checkout([$class: 'GitSCM', branches: [[name: '*/develop']], doGenerateSubmoduleConfigurations: false, extensions: [[$class: 'CleanBeforeCheckout']], submoduleCfg: [], userRemoteConfigs: [[url: 'https://github.com/status-im/open-bounty.git']]]) configFileProvider([configFile(fileId: 'sob_automation_test_config', targetLocation: 'test/end-to-end/tests')]) { try {withCredentials([string(credentialsId: 'SOB_SAUCE_ACCESS_KEY', variable: 'SAUCE_ACCESS_KEY'), string(credentialsId: 'SOB_SAUCE_USERNAME', variable: 'SAUCE_USERNAME')]) - {sh 'cd test && docker build -t end2end . && docker run --rm -e "SAUCE_USERNAME="${SAUCE_USERNAME} -e "SAUCE_ACCESS_KEY="${SAUCE_ACCESS_KEY} --name end2end-container end2end -m pytest -m sanity --build=$BUILD_NAME -v -n 1'} + {sh 'cd test && docker build -t end2end . && docker run -v `pwd`/end-to-end/tests/results:/app/results --rm -e "SAUCE_USERNAME="${SAUCE_USERNAME} -e "SAUCE_ACCESS_KEY="${SAUCE_ACCESS_KEY} --name end2end-container end2end -m pytest -m sanity --build=$BUILD_NAME -v -n 1'} } finally { saucePublisher() - junit testDataPublishers: [[$class: 'SauceOnDemandReportPublisher', jobVisibility: 'public']], testResults: 'test/end-to-end/tests/*.xml' } + junit testDataPublishers: [[$class: 'SauceOnDemandReportPublisher', jobVisibility: 'public']], testResults: 'test/end-to-end/tests/results/*.xml' } } } } \ No newline at end of file diff --git a/test/end-to-end/pages/base_element.py b/test/end-to-end/pages/base_element.py index 3da2cb8..9bf739c 100644 --- a/test/end-to-end/pages/base_element.py +++ b/test/end-to-end/pages/base_element.py @@ -6,7 +6,6 @@ from selenium.webdriver.support import expected_conditions class BaseElement(object): - class Locator(object): def __init__(self, by, value): diff --git a/test/end-to-end/pages/base_page.py b/test/end-to-end/pages/base_page.py index f602775..de61cd4 100644 --- a/test/end-to-end/pages/base_page.py +++ b/test/end-to-end/pages/base_page.py @@ -15,4 +15,3 @@ class BasePageObject(object): @property def time_now(self): return datetime.now().strftime('%-m%-d%-H%-M%-S') - diff --git a/test/end-to-end/pages/openbounty/activity.py b/test/end-to-end/pages/openbounty/activity.py new file mode 100644 index 0000000..ce04384 --- /dev/null +++ b/test/end-to-end/pages/openbounty/activity.py @@ -0,0 +1,25 @@ +from pages.base_page import BasePageObject +from pages.base_element import * +from tests import test_data +import logging + + +class ActivityDescription(BaseText): + + def __init__(self, driver, status, issue_title): + super(ActivityDescription, self).__init__(driver) + self.locator = self.Locator.xpath_selector( + '//div[@class="description"]/div[contains(.,"%s")]/a[contains(.,"%s")]' % (status, issue_title)) + + +class ActivityPage(BasePageObject): + def __init__(self, driver): + super(ActivityPage, self).__init__(driver) + self.driver = driver + + def get_activity_page(self): + self.driver.get(test_data.config['Common']['url'] + 'app#/activity') + + def check_activity_is_presented(self, status, issue_title): + logging.info('Check that activity "%s %s" is displayed' % (status, issue_title)) + ActivityDescription(self.driver, status, issue_title).find_element() diff --git a/test/end-to-end/pages/openbounty/bounties.py b/test/end-to-end/pages/openbounty/bounties.py index 24a47bc..b18f91a 100644 --- a/test/end-to-end/pages/openbounty/bounties.py +++ b/test/end-to-end/pages/openbounty/bounties.py @@ -1,5 +1,6 @@ +import logging from pages.base_page import BasePageObject -from pages.base_element import * +from pages.base_element import BaseText from tests import test_data @@ -16,6 +17,7 @@ class TopHuntersHeader(BaseText): super(TopHuntersHeader, self).__init__(driver) self.locator = self.Locator.css_selector('.top-hunters-header') + class BountyTitles(BaseText): def __init__(self, driver): @@ -29,6 +31,7 @@ class BountyItemRows(BaseText): super(BountyItemRows, self).__init__(driver) self.locator = self.Locator.css_selector('.open-bounty-item-content .bounty-item-row') + class BountyFooters(BaseText): def __init__(self, driver): @@ -36,6 +39,14 @@ class BountyFooters(BaseText): self.locator = self.Locator.css_selector('.open-bounty-item-content .footer-row') +class BountyClaimsAmount(BaseText): + + def __init__(self, driver, issue_title, claims_text): + super(BaseText, self).__init__(driver) + self.locator = self.Locator.xpath_selector( + '//div[@class="header"]/a[contains(.,"%s")]/../../div[@class="footer-row"]/span[contains(.,"%s")]' % (issue_title, claims_text)) + + class BountiesPage(BasePageObject): def __init__(self, driver): super(BountiesPage, self).__init__(driver) @@ -51,3 +62,6 @@ class BountiesPage(BasePageObject): def get_bounties_page(self): self.driver.get(test_data.config['Common']['url'] + 'app') + def check_bounty_claims_amount(self, issue_title, claims_text): + logging.info('Check that bounty "%s" has "%s"' % (issue_title, claims_text)) + BountyClaimsAmount(self.driver, issue_title, claims_text).find_element() diff --git a/test/end-to-end/pages/openbounty/landing.py b/test/end-to-end/pages/openbounty/landing.py index 71f47f1..d22ef09 100644 --- a/test/end-to-end/pages/openbounty/landing.py +++ b/test/end-to-end/pages/openbounty/landing.py @@ -1,5 +1,5 @@ from pages.base_page import BasePageObject -from pages.base_element import * +from pages.base_element import BaseButton from tests import test_data diff --git a/test/end-to-end/pages/thirdparty/github.py b/test/end-to-end/pages/thirdparty/github.py index 60333be..cd2d347 100644 --- a/test/end-to-end/pages/thirdparty/github.py +++ b/test/end-to-end/pages/thirdparty/github.py @@ -1,11 +1,8 @@ -import time, pytest -from pages.base_element import * +import time, pytest, git, os, shutil, logging +from selenium.common.exceptions import TimeoutException +from pages.base_element import BaseEditBox, BaseButton, BaseText from pages.base_page import BasePageObject from tests import test_data -from git import Repo -import os -import shutil -import logging class EmailEditbox(BaseEditBox): @@ -110,25 +107,30 @@ class ContractBody(BaseText): super(ContractBody, self).__init__(driver) self.locator = self.Locator.xpath_selector("//tbody//p[contains(text(), " "'Current balance: 0.000000 ETH')]") + + class IssueId(BaseText): def __init__(self, driver): super(IssueId, self).__init__(driver) self.locator = self.Locator.css_selector(".gh-header-number") + class ForkButton(BaseButton): def __init__(self, driver): super(ForkButton, self).__init__(driver) self.locator = self.Locator.css_selector("[href='#fork-destination-box']") + class HeaderInForkPopup(BaseText): def __init__(self, driver): super(HeaderInForkPopup, self).__init__(driver) self.locator = self.Locator.css_selector("#facebox-header") + class UserAccountInForkPopup(BaseButton): def __init__(self, driver): super(UserAccountInForkPopup, self).__init__(driver) - self.locator = self.Locator.css_selector("[value=%s]"%test_data.config['DEV']['gh_username']) + self.locator = self.Locator.css_selector("[value=%s]" % test_data.config['DEV']['gh_username']) class ForkedRepoText(BaseText): @@ -136,20 +138,45 @@ class ForkedRepoText(BaseText): super(ForkedRepoText, self).__init__(driver) self.locator = self.Locator.css_selector(".commit-tease") + class DeleteRepo(BaseButton): def __init__(self, driver): super(DeleteRepo, self).__init__(driver) self.locator = self.Locator.xpath_selector("//button[text()[contains(.,' Delete this repository')]]") + class RepoNameBoxInPopup(BaseEditBox): def __init__(self, driver): super(RepoNameBoxInPopup, self).__init__(driver) - self.locator = self.Locator.css_selector("input[aria-label='Type in the name of the repository to confirm that you want to delete this repository.']") + self.locator = self.Locator.css_selector( + "input[aria-label='Type in the name of the repository to confirm that you want to delete this repository.']") + class ConfirmDeleteButton(BaseButton): def __init__(self, driver): super(ConfirmDeleteButton, self).__init__(driver) - self.locator = self.Locator.xpath_selector("//button[text()[contains(.,'I understand the consequences, delete')]]") + self.locator = self.Locator.xpath_selector( + "//button[text()[contains(.,'I understand the consequences, delete')]]") + + +class CompareAndPullRequest(BaseButton): + def __init__(self, driver): + super(CompareAndPullRequest, self).__init__(driver) + self.locator = self.Locator.css_selector(".RecentBranches a") + + +class PrTitleEditBox(BaseEditBox): + def __init__(self, driver): + super(PrTitleEditBox, self).__init__(driver) + self.locator = self.Locator.id("pull_request_body") + + +class SubmitNewPrButton(BaseButton): + def __init__(self, driver): + super(SubmitNewPrButton, self).__init__(driver) + self.locator = self.Locator.xpath_selector("//button[contains(text(), " + "'Create pull request')]") + class GithubPage(BasePageObject): def __init__(self, driver): @@ -157,18 +184,18 @@ class GithubPage(BasePageObject): self.driver = driver + # sign in and installation related 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) + # issue related self.new_issue_button = NewIssueButton(self.driver) self.issue_title_input = IssueTitleEditBox(self.driver) self.labels_button = LabelsButton(self.driver) @@ -177,21 +204,28 @@ class GithubPage(BasePageObject): self.submit_new_issue_button = SubmitNewIssueButton(self.driver) self.contract_body = ContractBody(self.driver) self.issue_id = IssueId(self.driver) + + # repo forking self.fork_button = ForkButton(self.driver) self.header_in_fork_popup = HeaderInForkPopup(self.driver) self.user_account_in_fork_popup = UserAccountInForkPopup(self.driver) self.forked_repo_text = ForkedRepoText(self.driver) + # repo deleting self.delete_repo = DeleteRepo(self.driver) self.repo_name_confirm_delete = RepoNameBoxInPopup(self.driver) self.confirm_delete = ConfirmDeleteButton(self.driver) + # PR related + self.compare_and_pull_request = CompareAndPullRequest(self.driver) + self.pr_body = PrTitleEditBox(self.driver) + self.submit_new_pr_button = SubmitNewPrButton(self.driver) def get_issues_page(self): - self.driver.get(test_data.config['ORG']['gh_repo'] + 'issues') + self.driver.get('%sissues' % test_data.config['ORG']['gh_repo']) def get_issue_page(self, issue_id): - self.driver.get(test_data.config['ORG']['gh_repo'] + 'issues/' + issue_id) + self.driver.get('%sissues/%s' % (test_data.config['ORG']['gh_repo'], issue_id)) def get_sob_plugin_page(self): self.driver.get(test_data.config['Common']['sob_test_app']) @@ -201,7 +235,6 @@ class GithubPage(BasePageObject): 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() @@ -215,14 +248,14 @@ class GithubPage(BasePageObject): self.get_issues_page() self.new_issue_button.click() test_data.issue = dict() - test_data.issue['title'] = 'auto_test_bounty_%s' % self.time_now + test_data.issue['title'] = 'auto_test_bounty_%s' % test_data.date_time self.issue_title_input.send_keys(test_data.issue['title']) self.labels_button.click() self.bounty_label.click() self.cross_button.click() self.submit_new_issue_button.click() test_data.issue['id'] = self.issue_id.text[1:] - logging.info("Issue title is %s" % test_data.issue['title']) + logging.info("Issue title is %s" % test_data.issue['title']) def fork_repo(self, initial_repo, wait=60): self.driver.get(initial_repo) @@ -242,35 +275,49 @@ class GithubPage(BasePageObject): except TimeoutException: time.sleep(10) pass - pytest.fail('Contract is not deployed in %s minutes!' % str(wait/60)) + pytest.fail('Contract is not deployed in %s minutes!' % str(wait / 60)) - #cloning via HTTPS - def clone_repo(self, initial_repo=None, username=None, repo_name=None, repo_path='git_repo'): - os.mkdir(repo_path) - os.chdir(repo_path) - test_data.local_repo_path = os.getcwd() + # cloning via HTTPS + def clone_repo(self, initial_repo=None, username=None, repo_name=None, repo_folder='test_repo'): + os.mkdir(repo_folder) + os.chdir(repo_folder) + self.local_repo_path = os.getcwd() fork = 'https://github.com/%s/%s.git' % (username, repo_name) - logging.info(('Cloning from %s to %s' % (fork, repo_path))) - r = Repo.clone_from(fork, repo_path) - logging.info(('Successefully cloned to: %s' % test_data.local_repo_path)) - logging.info('Set upstream to %s'% initial_repo) - upstream = r.create_remote('upstream', initial_repo) + logging.info(('Cloning from %s to %s' % (fork, self.local_repo_path))) + repo = git.Repo.clone_from(fork, self.local_repo_path) + logging.info(('Successefully cloned to: %s' % self.local_repo_path)) + logging.info('Set upstream to %s' % initial_repo) + upstream = repo.create_remote('upstream', initial_repo) upstream.fetch() assert upstream.exists() - r.heads.master.checkout() + repo.heads.master.checkout() + + def create_pr_git(self, branch, file_to_modify='test'): + repo = git.Repo(self.local_repo_path) + logging.info(repo.git.status()) + logging.info(repo.git.pull('upstream', 'master')) + logging.info(repo.git.push('origin', 'master')) + logging.info(repo.git.fetch('--all')) + repo.git.checkout('-b', branch) + file = open(os.path.join(self.local_repo_path, file_to_modify), 'w') + file.write("Autotest change: %s \r \n" % test_data.date_time) + logging.info(repo.git.add('test')) + logging.info(repo.git.commit(m='Aut %s' % test_data.date_time)) + repo.git.push('origin', branch) + + def open_pr_github(self, keyword_comment): + self.get_url(test_data.config['DEV']['gh_forked_repo']) + self.compare_and_pull_request.click() + self.pr_body.send_keys('%s #%s' % (keyword_comment, test_data.issue['id'])) + self.submit_new_pr_button.click() def clean_repo_local_folder(self): - logging.info('Removing %s' % test_data.local_repo_path) - if test_data.local_repo_path: - shutil.rmtree(test_data.local_repo_path) + logging.info('Removing %s' % self.local_repo_path) + if self.local_repo_path: + shutil.rmtree(self.local_repo_path) def delete_fork(self): self.get_url(test_data.config['DEV']['gh_forked_repo'] + 'settings') self.delete_repo.click() self.repo_name_confirm_delete.send_keys(test_data.config['ORG']['gh_repo_name']) self.confirm_delete.click() - - - - - diff --git a/test/end-to-end/pages/thirdparty/metamask_plugin.py b/test/end-to-end/pages/thirdparty/metamask_plugin.py index db11190..9d04538 100644 --- a/test/end-to-end/pages/thirdparty/metamask_plugin.py +++ b/test/end-to-end/pages/thirdparty/metamask_plugin.py @@ -1,6 +1,6 @@ import time from pages.base_page import BasePageObject -from pages.base_element import * +from pages.base_element import BaseButton, BaseText, BaseEditBox from selenium.webdriver import ActionChains @@ -74,7 +74,6 @@ class MetaMaskPlugin(BasePageObject): 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() @@ -85,4 +84,3 @@ class MetaMaskPlugin(BasePageObject): 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 index 89c0c76..1964f28 100644 --- a/test/end-to-end/pytest.ini +++ b/test/end-to-end/pytest.ini @@ -1,3 +1,3 @@ [pytest] norecursedirs = .git pages -addopts = -s -v --junitxml=result.xml --tb=short +addopts = -s -v --junitxml=results/result.xml --tb=short diff --git a/test/end-to-end/tests/__init__.py b/test/end-to-end/tests/__init__.py index 5277395..5a91935 100644 --- a/test/end-to-end/tests/__init__.py +++ b/test/end-to-end/tests/__init__.py @@ -1,5 +1,5 @@ -import configparser -import os +import configparser, time, datetime, os + class TestData(object): @@ -7,15 +7,17 @@ class TestData(object): self.test_name = None self.config = configparser.ConfigParser() - # define here path to your config.ini file - # example - config_example.ini + # put config.ini to /test/end-to-end/tests folder (same directory where config_example.ini is placed + self.tests_path = os.path.abspath(os.path.dirname(__file__)) + self.config.read(os.path.join(self.tests_path, 'config.ini')) - self.config.read(os.path.join(os.path.abspath(os.path.dirname(__file__)), 'config.ini')) + # create unique identificator for PRs, issues ect + ts = time.time() + st = datetime.datetime.fromtimestamp(ts).strftime('%Y-%m-%d %H:%M:%S') + self.date_time = st # self.issue['title'] is set in GithubPage::create_new_bounty # self.issue['id'] is set in GithubPage::create_new_bounty - # self.local_repo_path is set in GithubPage::clone_repo - test_data = TestData() diff --git a/test/end-to-end/tests/basetestcase.py b/test/end-to-end/tests/basetestcase.py index 92e30cc..34c2ae1 100644 --- a/test/end-to-end/tests/basetestcase.py +++ b/test/end-to-end/tests/basetestcase.py @@ -1,19 +1,20 @@ -import pytest, sys +import pytest, sys, os from selenium import webdriver from selenium.common.exceptions import WebDriverException from tests.postconditions import remove_application, remove_installation -from os import environ, path +from os import environ from tests import test_data from pages.thirdparty.github import GithubPage from pages.openbounty.landing import LandingPage -class BaseTestCase: +class BaseTestCase: def print_sauce_lab_info(self, driver): sys.stdout = sys.stderr print("SauceOnDemandSessionID=%s job-name=%s" % (driver.session_id, pytest.config.getoption('build'))) + def get_remote_caps(self): sauce_lab_cap = dict() sauce_lab_cap['name'] = test_data.test_name @@ -36,11 +37,11 @@ class BaseTestCase: @classmethod def setup_class(cls): cls.errors = [] - cls.environment = pytest.config.getoption('env') + cls.environment = pytest.config.getoption('env') -################################################################################################################### -######### Drivers setup -################################################################################################################### + ################################################################################################################ + ######### Drivers setup + ################################################################################################################ # # Dev Chrome options @@ -52,20 +53,20 @@ class BaseTestCase: # Org Chrome options # cls.capabilities_org = webdriver.ChromeOptions() - # doesn't work on sauce env - # cls.capabilities_org.add_extension(path.abspath(test_data.config['Paths']['tests_absolute'] + 'resources/metamask3_12_0.crx')) + cls.capabilities_org.add_extension( + os.path.join(test_data.tests_path, os.pardir, 'resources', 'metamask3_12_0.crx')) # # SauceLab capabilities # cls.executor_sauce_lab = 'http://%s:%s@ondemand.saucelabs.com:80/wd/hub' % ( - environ.get('SAUCE_USERNAME'), environ.get('SAUCE_ACCESS_KEY')) - drivers = [] + environ.get('SAUCE_USERNAME'), environ.get('SAUCE_ACCESS_KEY')) + cls.drivers = [] if cls.environment == 'local': for caps in cls.capabilities_dev, cls.capabilities_org: driver = webdriver.Chrome(chrome_options=caps) - drivers.append(driver) + cls.drivers.append(driver) if cls.environment == 'sauce': for caps in cls.capabilities_dev, cls.capabilities_org: @@ -74,30 +75,26 @@ class BaseTestCase: new_caps.update(remote) driver = webdriver.Remote(cls.executor_sauce_lab, desired_capabilities=new_caps) - drivers.append(driver) + cls.drivers.append(driver) - for driver in drivers: - cls.print_sauce_lab_info(cls, driver) + cls.driver_dev = cls.drivers[0] + cls.driver_org = cls.drivers[1] - cls.driver_dev = drivers[0] - cls.driver_org = drivers[1] + for driver in cls.drivers: + driver.implicitly_wait(10) - - for driver in drivers: - driver.implicitly_wait(10) - -################################################################################################################### -######### Actions for each driver before class -################################################################################################################### + ################################################################################################################ + ######### Actions for each driver before class + ################################################################################################################ ######ORG landing = LandingPage(cls.driver_org) landing.get_landing_page() - # Sign Up to SOB + # Sign Up to SOB cls.github_org = landing.login_button.click() cls.github_org.sign_in(test_data.config['ORG']['gh_login'], - test_data.config['ORG']['gh_password']) + test_data.config['ORG']['gh_password']) assert cls.github_org.permission_type.text == 'Personal user data' bounties_page = cls.github_org.authorize_sob.click() @@ -111,21 +108,17 @@ class BaseTestCase: # Sign In to GH as Developer cls.github_dev.get_login_page() cls.github_dev.sign_in(test_data.config['DEV']['gh_login'], - test_data.config['DEV']['gh_password']) + test_data.config['DEV']['gh_password']) - # Fork repo as Developer from Organization + # Fork repo as Developer from Organization cls.github_dev.fork_repo(test_data.config['ORG']['gh_repo']) - # Cloning repo to local git as Developer and set upstream to Organization (via HTTPS) + # Cloning repo to local git as Developer and set upstream to Organization (via HTTPS) cls.github_dev.clone_repo(test_data.config['ORG']['gh_repo'], - test_data.config['DEV']['gh_username'], - test_data.config['ORG']['gh_repo_name'], - 'git_repo') + test_data.config['DEV']['gh_username'], + test_data.config['ORG']['gh_repo_name']) cls.verify_no_errors(cls) - - - @classmethod def teardown_class(cls): @@ -140,10 +133,9 @@ class BaseTestCase: cls.github_dev.delete_fork() try: - cls.driver_dev.quit() - cls.driver_org.quit() + for driver in cls.drivers: + cls.print_sauce_lab_info(cls, driver) + driver.quit() + except WebDriverException: pass - - - diff --git a/test/end-to-end/tests/postconditions.py b/test/end-to-end/tests/postconditions.py index 1b09d42..184bbb8 100644 --- a/test/end-to-end/tests/postconditions.py +++ b/test/end-to-end/tests/postconditions.py @@ -20,4 +20,3 @@ def remove_installation(driver): driver.find_element(By.CSS_SELECTOR, '.facebox-popup .btn-danger').click() except NoSuchElementException: pass - diff --git a/test/end-to-end/tests/results/.gitignore b/test/end-to-end/tests/results/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/test/end-to-end/tests/test_contracts.py b/test/end-to-end/tests/test_contracts.py index 17c66e2..61d6e2f 100644 --- a/test/end-to-end/tests/test_contracts.py +++ b/test/end-to-end/tests/test_contracts.py @@ -1,8 +1,6 @@ import pytest -from os import environ -from pages.openbounty.landing import LandingPage from pages.openbounty.bounties import BountiesPage -from pages.thirdparty.github import GithubPage +from pages.openbounty.activity import ActivityPage from tests.basetestcase import BaseTestCase from tests import test_data @@ -10,18 +8,32 @@ from tests import test_data @pytest.mark.sanity class TestLogin(BaseTestCase): - def test_deploy_new_contract(self): + def test_deploy_new_contract(self): # Waiting for deployed contract; test_data.issue created here self.github_org.create_new_bounty() self.github_org.get_deployed_contract() # Navigate and check top bounty in "Open bounties" - bounties_page = BountiesPage(self.driver_org) + bounties_page = BountiesPage(self.driver_dev) bounties_page.get_bounties_page() titles = bounties_page.bounty_titles.find_elements() assert titles[0].text == test_data.issue['title'] + def test_new_claim(self): + self.github_dev.create_pr_git('test_branch_%s' % self.github_dev.time_now) + self.github_dev.open_pr_github('Fixes') + + # check new claim in "Open bounties" + bounties_page = BountiesPage(self.driver_dev) + bounties_page.get_bounties_page() + bounties_page.check_bounty_claims_amount(test_data.issue['title'], '1 open claim') + + # check new claim in "Activity" + activity_page = ActivityPage(self.driver_dev) + activity_page.get_activity_page() + activity_page.check_activity_is_presented('Submitted a claim for ', test_data.issue['title']) +