From b810180f3aeb810ce45771a44b9f3a23c06474a2 Mon Sep 17 00:00:00 2001 From: gabriel-arc <57348209+GbArc@users.noreply.github.com> Date: Mon, 24 Jul 2023 08:17:23 +0200 Subject: [PATCH] New user journey dev (#1278) * Add reason for manual cancel in build cancellation * Add login of user who cancelled the build * Use user instead of current user * Use RSS token for builds atom feed re #BSFY-206 * Change reason for cancellation * Revert "Add reason for manual cancel in build cancellation" * New payment details implementation * Antifraud for v1 plans * Add spec * Fix spec * Take recaptcha secret from env * storage,collaborator cleanup , specs * redirection for app updates * using vcs_redirect only if there's no state param * force redirection to github-apps endpoint on install * corrected storage key name * storage fix * storage param name fix * auth with installation updates * dbg * Dt fingerprint fix (#1289) * fingerprint fix * spec --- lib/travis/api/app/endpoint/authorization.rb | 13 ++- lib/travis/api/v3/billing_client.rb | 16 +++- lib/travis/api/v3/models/allowance.rb | 7 +- lib/travis/api/v3/models/storage.rb | 38 ++++++++ lib/travis/api/v3/queries/storage.rb | 45 ++++++++++ lib/travis/api/v3/queries/subscription.rb | 40 +++++++++ lib/travis/api/v3/queries/user.rb | 21 +++++ lib/travis/api/v3/queries/v2_subscription.rb | 40 +++++++++ lib/travis/api/v3/recaptcha_client.rb | 41 +++++++++ lib/travis/api/v3/renderer/allowance.rb | 3 +- lib/travis/api/v3/renderer/storage.rb | 9 ++ lib/travis/api/v3/renderer/user.rb | 6 +- lib/travis/api/v3/routes.rb | 9 ++ lib/travis/api/v3/services.rb | 1 + lib/travis/api/v3/services/storage/delete.rb | 10 +++ lib/travis/api/v3/services/storage/find.rb | 10 +++ lib/travis/api/v3/services/storage/update.rb | 12 +++ .../subscription/update_payment_details.rb | 11 +++ .../v2_subscription/update_payment_details.rb | 11 +++ lib/travis/config/defaults.rb | 4 +- lib/travis/remote_vcs/user.rb | 3 +- .../services/repositories/for_owner_spec.rb | 44 +++++---- spec/v3/services/storage/delete_spec.rb | 45 ++++++++++ spec/v3/services/storage/find_spec.rb | 62 +++++++++++++ spec/v3/services/storage/update_spec.rb | 63 +++++++++++++ .../update_payment_details_spec.rb | 90 +++++++++++++++++++ spec/v3/services/user/find_spec.rb | 84 +++++++++++++++++ .../update_payment_details_spec.rb | 90 +++++++++++++++++++ 28 files changed, 802 insertions(+), 26 deletions(-) create mode 100644 lib/travis/api/v3/models/storage.rb create mode 100644 lib/travis/api/v3/queries/storage.rb create mode 100644 lib/travis/api/v3/recaptcha_client.rb create mode 100644 lib/travis/api/v3/renderer/storage.rb create mode 100644 lib/travis/api/v3/services/storage/delete.rb create mode 100644 lib/travis/api/v3/services/storage/find.rb create mode 100644 lib/travis/api/v3/services/storage/update.rb create mode 100644 lib/travis/api/v3/services/subscription/update_payment_details.rb create mode 100644 lib/travis/api/v3/services/v2_subscription/update_payment_details.rb create mode 100644 spec/v3/services/storage/delete_spec.rb create mode 100644 spec/v3/services/storage/find_spec.rb create mode 100644 spec/v3/services/storage/update_spec.rb create mode 100644 spec/v3/services/subscription/update_payment_details_spec.rb create mode 100644 spec/v3/services/v2_subscription/update_payment_details_spec.rb diff --git a/lib/travis/api/app/endpoint/authorization.rb b/lib/travis/api/app/endpoint/authorization.rb index aaed4bf17a..b158572a12 100644 --- a/lib/travis/api/app/endpoint/authorization.rb +++ b/lib/travis/api/app/endpoint/authorization.rb @@ -106,9 +106,15 @@ class Authorization < Endpoint get '/handshake/?:provider?' do method = org? ? :handshake : :vcs_handshake params[:provider] ||= 'github' + params[:signup] ||= false send(method) do |user, token, redirect_uri| if target_ok? redirect_uri + user[:installation] = params[:installation_id] content_type :html + if params[:setup_action] && params[:setup_action] == 'install' && params[:provider] == 'github' + redirect_uri = redirect_uri + "?installation_id=#{params[:installation_id]}" + redirect_uri = "#{Travis.config.vcs_redirects.web_url}#{Travis.config.vcs_redirects[params[:provider]]}?installation_id=#{params[:installation_id]}" + end data = { user: user, token: token, uri: redirect_uri } erb(:post_payload, locals: data) else @@ -214,6 +220,10 @@ def remote_vcs_user def vcs_handshake if params[:code] + if params[:setup_action] && (params[:setup_action] == 'update' || params[:setup_action] == 'install') && params[:provider] && !params[:state] + redirect to("#{Travis.config.vcs_redirects.web_url}#{Travis.config.vcs_redirects[params[:provider]]}?installation_id=#{params[:installation_id]}") + end + unless state_ok?(params[:state], params[:provider]) handle_invalid_response return @@ -239,7 +249,8 @@ def vcs_handshake vcs_data = remote_vcs_user.auth_request( provider: params[:provider], state: state, - redirect_uri: oauth_endpoint + redirect_uri: oauth_endpoint, + signup: params[:signup] ) response.set_cookie(cookie_name(params[:provider]), value: state, httponly: true) diff --git a/lib/travis/api/v3/billing_client.rb b/lib/travis/api/v3/billing_client.rb index 1c7f5aaf77..fa7f0ea8c5 100644 --- a/lib/travis/api/v3/billing_client.rb +++ b/lib/travis/api/v3/billing_client.rb @@ -13,7 +13,7 @@ def allowance(owner_type, owner_id) response = connection(timeout: ALLOWANCE_TIMEOUT).get("/usage/#{owner_type.downcase}s/#{owner_id}/allowance") return BillingClient.default_allowance_response unless response.status == 200 - Travis::API::V3::Models::Allowance.new(2, owner_id, response.body) + Travis::API::V3::Models::Allowance.new(response.body.fetch('subscription_type', 2), owner_id, response.body) end def authorize_build(repo, sender_id, jobs) @@ -27,7 +27,11 @@ def self.default_allowance_response(id = 0) "private_repos" => false, "concurrency_limit" => 1, "user_usage" => false, - "pending_user_licenses" => false + "pending_user_licenses" => false, + "payment_changes_block_captcha" => false, + "payment_changes_block_credit" => false, + "credit_card_block_duration" => 0, + "captcha_block_duration" => 0 }.freeze) end @@ -239,6 +243,14 @@ def cancel_v2_subscription(id, reason_data) handle_subscription_response(response) end + def usage_stats(owners) + data = connection.post("/usage/stats", owners: owners, query: 'paid_plan_count') + data = data&.body + data.fetch('paid_plans').to_i > 0 if data && data['paid_plans'] + rescue + false + end + private def handle_subscription_response(response) diff --git a/lib/travis/api/v3/models/allowance.rb b/lib/travis/api/v3/models/allowance.rb index 984b749950..884bbdeec5 100644 --- a/lib/travis/api/v3/models/allowance.rb +++ b/lib/travis/api/v3/models/allowance.rb @@ -1,6 +1,7 @@ module Travis::API::V3 class Models::Allowance - attr_reader :subscription_type, :public_repos, :private_repos, :concurrency_limit, :user_usage, :pending_user_licenses, :id + attr_reader :subscription_type, :public_repos, :private_repos, :concurrency_limit, :user_usage, :pending_user_licenses, :id, + :payment_changes_block_credit, :payment_changes_block_captcha, :credit_card_block_duration, :captcha_block_duration def initialize(subscription_type, owner_id, attributes = {}) @subscription_type = subscription_type @@ -11,6 +12,10 @@ def initialize(subscription_type, owner_id, attributes = {}) @concurrency_limit = attributes.fetch('concurrency_limit', nil) @user_usage = attributes.fetch('user_usage', nil) @pending_user_licenses = attributes.fetch('pending_user_licenses', nil) + @payment_changes_block_captcha = attributes.fetch('payment_changes_block_captcha', nil) + @payment_changes_block_credit = attributes.fetch('payment_changes_block_credit', nil) + @credit_card_block_duration = attributes.fetch('credit_card_block_duration', nil) + @captcha_block_duration = attributes.fetch('captcha_block_duration', nil) end end end diff --git a/lib/travis/api/v3/models/storage.rb b/lib/travis/api/v3/models/storage.rb new file mode 100644 index 0000000000..047b7d66ce --- /dev/null +++ b/lib/travis/api/v3/models/storage.rb @@ -0,0 +1,38 @@ +require 'redis' + +module Travis::API::V3 + class Models::Storage + + attr_reader :id, :value + + def initialize(attrs) + puts "ATTRS: #{attrs.inspect}" + @id = attrs.fetch(:id) + @value = attrs[:value] + end + + def public? + true + end + + def get + puts "ID: #{id.inspect}, VAL: #{value.inspect}" + @value = Travis.redis.get(id) || 0 + + + puts "ID: #{id.inspect}, VAL: #{value.inspect}" + self + end + + def create + Travis.redis.set(id, value) + self + end + + def delete + Travis.redis.del(id) + @value = 0 + self + end + end +end diff --git a/lib/travis/api/v3/queries/storage.rb b/lib/travis/api/v3/queries/storage.rb new file mode 100644 index 0000000000..cd797a73a2 --- /dev/null +++ b/lib/travis/api/v3/queries/storage.rb @@ -0,0 +1,45 @@ +module Travis::API::V3 + class Queries::Storage < Query + params :user, :id,:value, prefix: :storage + + PERMITTED_OPTIONS = [:billing_wizard_state] + + def find + Models::Storage.new(id: option_id).get if valid? + end + + def update + Models::Storage.new(id: option_id, value: value).create if valid? + end + + def delete + Models::Storage.new(id: option_id).delete if valid? + end + + private + def option_id + "#{user}::storage::#{id}" + end + + def valid? + PERMITTED_OPTIONS.include? id.to_sym + end + + + def user + params['user.id'] + end + + def id + params['id'] + end + + def value + params['value'] + end + + def redis + Travis.redis + end + end +end diff --git a/lib/travis/api/v3/queries/subscription.rb b/lib/travis/api/v3/queries/subscription.rb index 639082ca8d..e5f6db94a1 100644 --- a/lib/travis/api/v3/queries/subscription.rb +++ b/lib/travis/api/v3/queries/subscription.rb @@ -1,5 +1,31 @@ module Travis::API::V3 class Queries::Subscription < Query + def update_payment_details(user_id) + recaptcha_redis_key = "recaptcha_attempts_v1_#{params['subscription.id']}" + count = Travis.redis.get(recaptcha_redis_key)&.to_i + count = count.nil? ? 0 : count + if count > captcha_max_failed_attempts + raise ClientError, 'Error verifying reCAPTCHA, you have exausted your attempts, please wait.' + end + + result = recaptcha_client.verify(params['captcha_token']) + unless result + if count == 0 + Travis.redis.setex(recaptcha_redis_key, captcha_block_duration, count + 1) + else + ttl = Travis.redis.ttl(recaptcha_redis_key) + Travis.redis.setex(recaptcha_redis_key, ttl, count + 1) + end + raise ClientError, 'Error verifying reCAPTCHA, please try again.' + end + + address_data = params.dup.tap { |h| h.delete('subscription.id') } + address_data = address_data.tap { |h| h.delete('token') } + client = BillingClient.new(user_id) + client.update_address(params['subscription.id'], address_data) unless address_data.empty? + client.update_creditcard(params['subscription.id'], params['token']) if params.key?('token') + end + def update_address(user_id) address_data = params.dup.tap { |h| h.delete('subscription.id') } client = BillingClient.new(user_id) @@ -42,5 +68,19 @@ def pay(user_id) client = BillingClient.new(user_id) client.pay(params['subscription.id']) end + + private + + def recaptcha_client + @recaptcha_client ||= RecaptchaClient.new + end + + def captcha_block_duration + Travis.config.antifraud.captcha_block_duration + end + + def captcha_max_failed_attempts + Travis.config.antifraud.captcha_max_failed_attempts + end end end diff --git a/lib/travis/api/v3/queries/user.rb b/lib/travis/api/v3/queries/user.rb index 7d6a38bef9..8da30114bc 100644 --- a/lib/travis/api/v3/queries/user.rb +++ b/lib/travis/api/v3/queries/user.rb @@ -23,6 +23,27 @@ def find_by_email(email) end end + def collaborator?(id) + user = Models::User.find_by_id(id) if id + return false unless user + + owners=[] + user.organizations.each do |org| + owners << { + :id => org.id, + :type => 'Organization' + } + end + Models::Repository.where(id: user.shared_repositories_ids).uniq.pluck(:owner_id, :owner_type).each do |owner| + owners << { + :id => owner[0], + :type =>owner[1] + } + end + client = BillingClient.new(id) + client.usage_stats(owners) + end + def sync(user) raise AlreadySyncing if user.is_syncing? if Travis::Features.user_active?(:use_vcs, user) || !user.github? diff --git a/lib/travis/api/v3/queries/v2_subscription.rb b/lib/travis/api/v3/queries/v2_subscription.rb index 4df2c9e83e..cf69d2fb77 100644 --- a/lib/travis/api/v3/queries/v2_subscription.rb +++ b/lib/travis/api/v3/queries/v2_subscription.rb @@ -2,6 +2,32 @@ module Travis::API::V3 class Queries::V2Subscription < Query params :enabled, :threshold, :amount + def update_payment_details(user_id) + recaptcha_redis_key = "recaptcha_attempts_v2_#{params['subscription.id']}" + count = Travis.redis.get(recaptcha_redis_key)&.to_i + count = count.nil? ? 0 : count + if count > captcha_max_failed_attempts + raise ClientError, 'Error verifying reCAPTCHA, you have exausted your attempts, please wait.' + end + + result = recaptcha_client.verify(params['captcha_token']) + unless result + if count == 0 + Travis.redis.setex(recaptcha_redis_key, captcha_block_duration, count + 1) + else + ttl = Travis.redis.ttl(recaptcha_redis_key) + Travis.redis.setex(recaptcha_redis_key, ttl, count + 1) + end + raise ClientError, 'Error verifying reCAPTCHA, please try again.' + end + + address_data = params.dup.tap { |h| h.delete('subscription.id') } + address_data = address_data.tap { |h| h.delete('token') } + client = BillingClient.new(user_id) + client.update_v2_address(params['subscription.id'], address_data) unless address_data.empty? + client.update_v2_creditcard(params['subscription.id'], params['token'], params['fingerprint']) if params.key?('token') + end + def update_address(user_id) address_data = params.dup.tap { |h| h.delete('subscription.id') } client = BillingClient.new(user_id) @@ -60,5 +86,19 @@ def update_auto_refill(user_id, addon_id) client = BillingClient.new(user_id) client.update_auto_refill(addon_id, threshold, amount) end + + private + + def recaptcha_client + @recaptcha_client ||= RecaptchaClient.new + end + + def captcha_block_duration + Travis.config.antifraud.captcha_block_duration + end + + def captcha_max_failed_attempts + Travis.config.antifraud.captcha_max_failed_attempts + end end end diff --git a/lib/travis/api/v3/recaptcha_client.rb b/lib/travis/api/v3/recaptcha_client.rb new file mode 100644 index 0000000000..ad3d76cb52 --- /dev/null +++ b/lib/travis/api/v3/recaptcha_client.rb @@ -0,0 +1,41 @@ +module Travis::API::V3 + class RecaptchaClient + class ConfigurationError < StandardError; end + + def verify(token) + response = connection.post('/recaptcha/api/siteverify') do |req| + req.headers['Content-Type'] = 'application/x-www-form-urlencoded' + req.body = URI.encode_www_form({ secret: recaptcha_secret, response: token }) + end + handle_errors_and_respond(response) { |r| r['success'] } + end + + private + + def handle_errors_and_respond(response) + case response.status + when 200, 201 + yield(JSON.parse(response.body)) if block_given? + when 204 + true + else + raise Travis::API::V3::ServerError, 'ReCaptcha system error' + end + end + + def connection + @connection ||= Faraday.new(url: recaptcha_url, ssl: { ca_path: '/usr/lib/ssl/certs' }) do |conn| + conn.use OpenCensus::Trace::Integrations::FaradayMiddleware if Travis::Api::App::Middleware::OpenCensus.enabled? + conn.adapter Faraday.default_adapter + end + end + + def recaptcha_url + Travis.config.recaptcha.endpoint || raise(ConfigurationError, 'No recaptcha url configured') + end + + def recaptcha_secret + Travis.config.recaptcha.secret || raise(ConfigurationError, 'No recaptcha secret configured') + end + end +end diff --git a/lib/travis/api/v3/renderer/allowance.rb b/lib/travis/api/v3/renderer/allowance.rb index c8e6ee4847..f83616fcbf 100644 --- a/lib/travis/api/v3/renderer/allowance.rb +++ b/lib/travis/api/v3/renderer/allowance.rb @@ -1,6 +1,7 @@ module Travis::API::V3 class Renderer::Allowance < ModelRenderer representation(:minimal, :id) - representation(:standard, :subscription_type, :public_repos, :private_repos, :concurrency_limit, :user_usage, :pending_user_licenses, :id) + representation(:standard, :subscription_type, :public_repos, :private_repos, :concurrency_limit, :user_usage, :pending_user_licenses, + :payment_changes_block_credit, :payment_changes_block_captcha, :credit_card_block_duration, :captcha_block_duration, :id) end end diff --git a/lib/travis/api/v3/renderer/storage.rb b/lib/travis/api/v3/renderer/storage.rb new file mode 100644 index 0000000000..c43147cf90 --- /dev/null +++ b/lib/travis/api/v3/renderer/storage.rb @@ -0,0 +1,9 @@ +module Travis::API::V3 + class Renderer::Storage < ModelRenderer + representation(:standard,:id, :value) + + def id + model.id.split('::')&.last || id + end + end +end diff --git a/lib/travis/api/v3/renderer/user.rb b/lib/travis/api/v3/renderer/user.rb index 05b1ebf457..bab6112d53 100644 --- a/lib/travis/api/v3/renderer/user.rb +++ b/lib/travis/api/v3/renderer/user.rb @@ -3,7 +3,7 @@ module Travis::API::V3 class Renderer::User < Renderer::Owner representation(:standard, :email, :is_syncing, :synced_at, :recently_signed_up, :secure_user_hash, :ro_mode, :confirmed_at, :custom_keys) - representation(:additional, :emails) + representation(:additional, :emails, :collaborator) def email @model.email if show_emails? @@ -13,6 +13,10 @@ def emails show_emails? ? @model.emails.map(&:email) : [] end + def collaborator + query(:user).collaborator? @model.id + end + def secure_user_hash hmac_secret_key = Travis.config.intercom && Travis.config.intercom.hmac_secret_key.to_s OpenSSL::HMAC.hexdigest('sha256', hmac_secret_key, @model.id.to_s) if @model.id && hmac_secret_key diff --git a/lib/travis/api/v3/routes.rb b/lib/travis/api/v3/routes.rb index 4fed6949d0..b1604bfa52 100644 --- a/lib/travis/api/v3/routes.rb +++ b/lib/travis/api/v3/routes.rb @@ -330,6 +330,13 @@ module Routes delete :delete end + hidden_resource :storage do + route '/storage/{id}' + get :find + patch :update + delete :delete + end + hidden_resource :beta_migration_requests do route '/beta_migration_requests' @@ -373,6 +380,7 @@ module Routes hidden_resource :subscription do route '/subscription/{subscription.id}' + patch :update_payment_details, '/payment_details' patch :update_address, '/address' patch :update_creditcard, '/creditcard' patch :update_plan, '/plan' @@ -384,6 +392,7 @@ module Routes hidden_resource :v2_subscription do route '/v2_subscription/{subscription.id}' + patch :update_payment_details, '/payment_details' patch :update_address, '/address' patch :update_creditcard, '/creditcard' patch :changetofree, '/changetofree' diff --git a/lib/travis/api/v3/services.rb b/lib/travis/api/v3/services.rb index 06790974ce..e3565ec493 100644 --- a/lib/travis/api/v3/services.rb +++ b/lib/travis/api/v3/services.rb @@ -41,6 +41,7 @@ module Services Log = Module.new { extend Services } ScanResult = Module.new { extend Services } ScanResults = Module.new { extend Services } + Storage = Module.new { extend Services } Messages = Module.new { extend Services } Organization = Module.new { extend Services } Organizations = Module.new { extend Services } diff --git a/lib/travis/api/v3/services/storage/delete.rb b/lib/travis/api/v3/services/storage/delete.rb new file mode 100644 index 0000000000..828cac3e3b --- /dev/null +++ b/lib/travis/api/v3/services/storage/delete.rb @@ -0,0 +1,10 @@ +module Travis::API::V3 + class Services::Storage::Delete < Service + def run! + raise LoginRequired unless access_control.full_access_or_logged_in? + + params['user.id'] = access_control.user&.id + result query.delete + end + end +end diff --git a/lib/travis/api/v3/services/storage/find.rb b/lib/travis/api/v3/services/storage/find.rb new file mode 100644 index 0000000000..0aeddbc346 --- /dev/null +++ b/lib/travis/api/v3/services/storage/find.rb @@ -0,0 +1,10 @@ +module Travis::API::V3 + class Services::Storage::Find < Service + def run! + raise LoginRequired unless access_control.full_access_or_logged_in? + + params['user.id'] = access_control.user&.id + result query.find + end + end +end diff --git a/lib/travis/api/v3/services/storage/update.rb b/lib/travis/api/v3/services/storage/update.rb new file mode 100644 index 0000000000..394b4421b6 --- /dev/null +++ b/lib/travis/api/v3/services/storage/update.rb @@ -0,0 +1,12 @@ +module Travis::API::V3 + class Services::Storage::Update < Service + params :id, :value + + def run! + raise LoginRequired unless access_control.full_access_or_logged_in? + + params['user.id'] = access_control.user&.id + result query.update + end + end +end diff --git a/lib/travis/api/v3/services/subscription/update_payment_details.rb b/lib/travis/api/v3/services/subscription/update_payment_details.rb new file mode 100644 index 0000000000..bf80fe5f3c --- /dev/null +++ b/lib/travis/api/v3/services/subscription/update_payment_details.rb @@ -0,0 +1,11 @@ +module Travis::API::V3 + class Services::Subscription::UpdatePaymentDetails < Service + params :captcha_token, :token, :first_name, :last_name, :company, :address, :address2, :city, :country, :state, :vat_id, :zip_code, :billing_email, :has_local_registration + + def run! + raise LoginRequired unless access_control.full_access_or_logged_in? + query.update_payment_details(access_control.user.id) + no_content + end + end +end diff --git a/lib/travis/api/v3/services/v2_subscription/update_payment_details.rb b/lib/travis/api/v3/services/v2_subscription/update_payment_details.rb new file mode 100644 index 0000000000..fcd394d204 --- /dev/null +++ b/lib/travis/api/v3/services/v2_subscription/update_payment_details.rb @@ -0,0 +1,11 @@ +module Travis::API::V3 + class Services::V2Subscription::UpdatePaymentDetails < Service + params :captcha_token, :token, :first_name, :last_name, :company, :address, :address2, :city, :country, :state, :vat_id, :zip_code, :billing_email, :has_local_registration + + def run! + raise LoginRequired unless access_control.full_access_or_logged_in? + query.update_payment_details(access_control.user.id) + no_content + end + end +end diff --git a/lib/travis/config/defaults.rb b/lib/travis/config/defaults.rb index 3e6a50ed5d..6c6d8bd231 100644 --- a/lib/travis/config/defaults.rb +++ b/lib/travis/config/defaults.rb @@ -90,7 +90,9 @@ def fallback_logs_api_auth_token logs_api: { url: logs_api_url, token: logs_api_auth_token }, fallback_logs_api: { url: fallback_logs_api_auth_url, token: fallback_logs_api_auth_token }, scanner: {}, - insights: { endpoint: 'https://insights.travis-ci.dev/', auth_token: 'secret' } + insights: { endpoint: 'https://insights.travis-ci.dev/', auth_token: 'secret' }, + recaptcha: { endpoint: 'https://www.google.com', secret: ENV['RECAPTCHA_SECRET_KEY'] || '' }, + antifraud: { captcha_max_failed_attempts: 3, captcha_block_duration: 24, credit_card_max_failed_attempts: 3, credit_card_block_duration: 24 } default :_access => [:key] diff --git a/lib/travis/remote_vcs/user.rb b/lib/travis/remote_vcs/user.rb index 5c81e1801d..3b342b5846 100644 --- a/lib/travis/remote_vcs/user.rb +++ b/lib/travis/remote_vcs/user.rb @@ -6,12 +6,13 @@ module Travis class RemoteVCS class User < Client - def auth_request(provider: :github, redirect_uri:, state:) + def auth_request(provider: :github, redirect_uri:, state:, signup:) request(:get, __method__) do |req| req.url 'users/session/new' req.params['provider'] = provider req.params['redirect_uri'] = redirect_uri req.params['state'] = state + req.params['signup'] = signup end end diff --git a/spec/v3/services/repositories/for_owner_spec.rb b/spec/v3/services/repositories/for_owner_spec.rb index 89d286a84b..55207e547f 100644 --- a/spec/v3/services/repositories/for_owner_spec.rb +++ b/spec/v3/services/repositories/for_owner_spec.rb @@ -585,15 +585,19 @@ before { get("/v3/owner/svenfuchs/allowance", {}, headers) } example { expect(last_response).to be_ok } example { expect(JSON.load(body)).to be == { - "@representation" => "standard", - "@type" => "allowance", - "concurrency_limit" => 1, - "private_repos" => false, - "public_repos" => true, - "subscription_type" => 1, - "user_usage" => false, - "pending_user_licenses" => false, - "id" => 0 + "@representation" => "standard", + "@type" => "allowance", + "concurrency_limit" => 1, + "private_repos" => false, + "public_repos" => true, + "subscription_type" => 1, + "user_usage" => false, + "pending_user_licenses" => false, + "id" => 0, + "captcha_block_duration" => 0, + "credit_card_block_duration" => 0, + "payment_changes_block_captcha" => false, + "payment_changes_block_credit" => false }} end @@ -608,15 +612,19 @@ end example { expect(last_response).to be_ok } example { expect(JSON.load(body)).to be == { - "@representation" => "standard", - "@type" => "allowance", - "concurrency_limit" => 666, - "private_repos" => true, - "public_repos" => true, - "subscription_type" => 2, - "user_usage" => true, - "pending_user_licenses" => false, - "id" => 1 + "@representation" => "standard", + "@type" => "allowance", + "concurrency_limit" => 666, + "private_repos" => true, + "public_repos" => true, + "subscription_type" => 2, + "user_usage" => true, + "pending_user_licenses" => false, + "id" => 1, + "captcha_block_duration" => nil, + "credit_card_block_duration" => nil, + "payment_changes_block_captcha" => nil, + "payment_changes_block_credit" => nil }} end diff --git a/spec/v3/services/storage/delete_spec.rb b/spec/v3/services/storage/delete_spec.rb new file mode 100644 index 0000000000..ff62c9fdea --- /dev/null +++ b/spec/v3/services/storage/delete_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Travis::API::V3::Services::Storage::Delete, set_app: true do + let(:user) { FactoryBot.create(:user) } + let(:token) { Travis::Api::App::AccessToken.create(user: user, app_id: 1) } + let(:auth_headers) { { 'HTTP_AUTHORIZATION' => "token #{token}" } } + let(:id) { 'billing_wizard_state' } + let(:parsed_body) { JSON.load(last_response.body) } + + describe 'not authenticated' do + before { delete("/v3/storage/#{id}") } + example do + expect(last_response.status).to eq 403 + end + end + + describe 'authenticated, other user' do + let(:other_user) { FactoryBot.create(:user, login: 'noone') } + let(:token) { Travis::Api::App::AccessToken.create(user: other_user, app_id: 1) } + let(:auth_headers) { { 'HTTP_AUTHORIZATION' => "token #{token}" } } + before { delete("/v3/storage/#{id}", auth_headers) } + example do + expect(last_response.status).to eq 403 + end + end + + context 'authenticated, right permissions' do + describe 'existing user' do + before do + delete("/v3/storage/#{id}", {}, auth_headers) + end + example do + expect(last_response.status).to eq 200 + expect(parsed_body).to eql_json({ + '@type' => 'storage', + '@representation' => 'standard', + 'id' => 'billing_wizard_state', + 'value' => 0 + }) + end + end + end +end diff --git a/spec/v3/services/storage/find_spec.rb b/spec/v3/services/storage/find_spec.rb new file mode 100644 index 0000000000..d27b9e836f --- /dev/null +++ b/spec/v3/services/storage/find_spec.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Travis::API::V3::Services::Storage::Find, set_app: true do + let(:user) { FactoryBot.create(:user) } + let(:token) { Travis::Api::App::AccessToken.create(user: user, app_id: 1) } + let(:auth_headers) { { 'HTTP_AUTHORIZATION' => "token #{token}" } } + let(:id) { 'billing_wizard_state' } + let(:parsed_body) { JSON.load(last_response.body) } + + describe 'not authenticated' do + before { get("/v3/storage/#{id}") } + example do + expect(last_response.status).to eq 403 + end + end + + describe 'authenticated, other user' do + let(:other_user) { FactoryBot.create(:user, login: 'noone') } + let(:token) { Travis::Api::App::AccessToken.create(user: other_user, app_id: 1) } + let(:auth_headers) { { 'HTTP_AUTHORIZATION' => "token #{token}" } } + before { get("/v3/storage/#{id}", auth_headers) } + example do + expect(last_response.status).to eq 403 + end + end + + context 'authenticated, right permissions' do + describe 'existing user, missing key' do + before do + delete("/v3/storage/#{id}", {}, auth_headers) + get("/v3/storage/#{id}", {}, auth_headers) + end + example do + expect(last_response.status).to eq 200 + expect(parsed_body).to eql_json({ + '@type' => 'storage', + '@representation' => 'standard', + 'id' => 'billing_wizard_state', + 'value' => 0 + }) + end + end + + describe 'existing user, proper key' do + before do + patch("/v3/storage/#{id}", { 'value': 1 }, auth_headers) + get("/v3/storage/#{id}", {}, auth_headers) + end + example do + expect(last_response.status).to eq 200 + expect(parsed_body).to eql_json({ + '@type' => 'storage', + '@representation' => 'standard', + 'id' => 'billing_wizard_state', + 'value' => '1' + }) + end + end + end +end diff --git a/spec/v3/services/storage/update_spec.rb b/spec/v3/services/storage/update_spec.rb new file mode 100644 index 0000000000..e0d5bdaa90 --- /dev/null +++ b/spec/v3/services/storage/update_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Travis::API::V3::Services::Storage::Update, set_app: true do + let(:user) { FactoryBot.create(:user) } + let(:token) { Travis::Api::App::AccessToken.create(user: user, app_id: 1) } + let(:auth_headers) { { 'HTTP_AUTHORIZATION' => "token #{token}" } } + let(:id) { 'billing_wizard_state' } + let(:parsed_body) { JSON.load(last_response.body) } + + describe 'not authenticated' do + before { patch("/v3/storage/#{id}") } + example do + expect(last_response.status).to eq 403 + end + end + + describe 'authenticated, other user' do + let(:other_user) { FactoryBot.create(:user, login: 'noone') } + let(:token) { Travis::Api::App::AccessToken.create(user: other_user, app_id: 1) } + let(:auth_headers) { { 'HTTP_AUTHORIZATION' => "token #{token}" } } + before { patch("/v3/storage/#{id}", auth_headers) } + example do + expect(last_response.status).to eq 403 + end + end + + context 'authenticated, right permissions' do + describe 'existing user, missing key' do + before do + delete("/v3/storage/#{id}", {}, auth_headers) + patch("/v3/storage/#{id}", { 'value': 33 }, auth_headers) + end + example do + expect(last_response.status).to eq 200 + expect(parsed_body).to eql_json({ + '@type' => 'storage', + '@representation' => 'standard', + 'id' => 'billing_wizard_state', + 'value' => '33' + }) + end + end + + describe 'existing user, existing key' do + before do + patch("/v3/storage/#{id}", { 'value': 33 }, auth_headers) + patch("/v3/storage/#{id}", { 'value': 44 }, auth_headers) + get("/v3/storage/#{id}", {}, auth_headers) + end + example do + expect(last_response.status).to eq 200 + expect(parsed_body).to eql_json({ + '@type' => 'storage', + '@representation' => 'standard', + 'id' => 'billing_wizard_state', + 'value' => '44' + }) + end + end + end +end diff --git a/spec/v3/services/subscription/update_payment_details_spec.rb b/spec/v3/services/subscription/update_payment_details_spec.rb new file mode 100644 index 0000000000..a812a001dc --- /dev/null +++ b/spec/v3/services/subscription/update_payment_details_spec.rb @@ -0,0 +1,90 @@ +describe Travis::API::V3::Services::Subscription::UpdatePaymentDetails, set_app: true, billing_spec_helper: true do + let(:billing_url) { 'http://billingfake.travis-ci.com' } + let(:billing_auth_key) { 'secret' } + + before do + Travis.config.billing.url = billing_url + Travis.config.billing.auth_key = billing_auth_key + Travis.config.antifraud.captcha_block_duration = 24 + end + + context 'unauthenticated' do + it 'responds 403' do + patch('/v3/subscription/123/payment_details', { }) + + expect(last_response.status).to eq(403) + end + end + + context 'authenticated' do + let(:user) { FactoryBot.create(:user) } + let(:token) { Travis::Api::App::AccessToken.create(user: user, app_id: 1) } + let(:headers) {{ 'HTTP_AUTHORIZATION' => "token #{token}", + 'CONTENT_TYPE' => 'application/json' }} + let(:address_data) { { + 'address' => 'Rigaer Strasse', + 'first_name' => 'Travis', + 'last_name' => 'Schmidt', + 'company' => 'Travis', + 'city' => 'Berlin', + 'country' => 'Germany', + 'zip_code' => '10001', + 'billing_email' => 'travis@example.org', + 'token' => 'token_from_stripe' + } } + let(:subscription_id) { rand(999) } + + let!(:stubbed_request_address) do + stub_billing_request(:patch, "/subscriptions/#{subscription_id}/address", auth_key: billing_auth_key, user_id: user.id) + .with(body: { + 'first_name' => 'Travis', + 'last_name' => 'Schmidt', + 'company' => 'Travis', + 'address' => 'Rigaer Strasse', + 'city' => 'Berlin', + 'country' => 'Germany', + 'zip_code' => '10001', + 'billing_email' => 'travis@example.org' + }) + .to_return(status: 204) + end + + let!(:stubbed_request_creditcard) do + stub_billing_request(:patch, "/subscriptions/#{subscription_id}/creditcard", auth_key: billing_auth_key, user_id: user.id) + .with(body: { 'token' => 'token_from_stripe' }) + .to_return(status: 204) + end + + context 'user is clean' do + before do + Travis.redis.del("recaptcha_attempts_v1_#{subscription_id}") + end + + it 'updates the address and credit card' do + allow_any_instance_of(Travis::API::V3::RecaptchaClient).to receive(:verify).and_return(true) + + patch("/v3/subscription/#{subscription_id}/payment_details", JSON.generate(address_data), headers) + + expect(last_response.status).to eq(204) + expect(stubbed_request_address).to have_been_made.once + expect(stubbed_request_creditcard).to have_been_made.once + end + end + + context 'user failed captcha check' do + before do + Travis.redis.setex("recaptcha_attempts_v1_#{subscription_id}", Travis.config.antifraud.captcha_block_duration, 1) + end + + it 'updates the address and credit card' do + allow_any_instance_of(Travis::API::V3::RecaptchaClient).to receive(:verify).and_return(false) + + patch("/v3/subscription/#{subscription_id}/payment_details", JSON.generate(address_data), headers) + + expect(last_response.status).to eq(400) + expect(stubbed_request_address).to_not have_been_made + expect(stubbed_request_creditcard).to_not have_been_made + end + end + end +end diff --git a/spec/v3/services/user/find_spec.rb b/spec/v3/services/user/find_spec.rb index b544918ccf..4da163f8d5 100644 --- a/spec/v3/services/user/find_spec.rb +++ b/spec/v3/services/user/find_spec.rb @@ -6,6 +6,7 @@ let(:billing_url) { 'http://billingfake.travis-ci.com' } let(:billing_auth_key) { 'secret' } + let(:paid_plans_count) { 1 } before do user.education = true @@ -15,6 +16,8 @@ Travis.config.billing.auth_key = billing_auth_key stub_billing_request(:get, "/usage/users/#{user.id}/allowance", auth_key: billing_auth_key, user_id: user.id) .to_return(body: JSON.dump({ 'public_repos': true, 'private_repos': true, 'user_usage': true, 'pending_user_licenses': false, 'concurrency_limit': 666 })) + stub_billing_request(:post, "/usage/stats", auth_key: billing_auth_key, user_id: user.id) + .to_return(body: JSON.dump({ 'query': 'paid_plan_count', 'paid_plans': paid_plans_count })) end describe "authenticated as user with access" do @@ -49,4 +52,85 @@ "confirmed_at" => nil, }} end + + describe "authenticated as user with access ,collaboration status" do + + let(:paid_plans_count) { 1 } + before { + get("/v3/user/#{user.id}?include=user.collaborator", {}, headers) + + } + example { + expect(last_response).to be_ok + } + example { expect(JSON.load(body)).to be == { + "@type" => "user", + "@href" => "/v3/user/#{user.id}", + "@representation" => "standard", + "@permissions" => {"read"=>true, "sync"=>true}, + "id" => user.id, + "login" => "svenfuchs", + "name" => "Sven Fuchs", + "email" => "sven@fuchs.com", + "github_id" => user.github_id, + "vcs_id" => user.vcs_id, + "vcs_type" => user.vcs_type, + "avatar_url" => "https://0.gravatar.com/avatar/07fb84848e68b96b69022d333ca8a3e2", + "is_syncing" => user.is_syncing, + "synced_at" => user.synced_at, + "education" => true, + "allow_migration" => false, + "allowance" => { + "@type" => "allowance", + "@representation" => "minimal", + "id" => user.id + }, + "custom_keys" => [], + "recently_signed_up"=>false, + "secure_user_hash" => nil, + "ro_mode" => false, + "confirmed_at" => nil, + 'collaborator' => true + }} + end + + describe "authenticated as user with access ,collaboration status when user is not a collaborator" do + let(:paid_plans_count) { 0 } + before { + get("/v3/user/#{user.id}?include=user.collaborator", {}, headers) + } + + example { + expect(last_response).to be_ok + } + example { expect(JSON.load(body)).to be == { + "@type" => "user", + "@href" => "/v3/user/#{user.id}", + "@representation" => "standard", + "@permissions" => {"read"=>true, "sync"=>true}, + "id" => user.id, + "login" => "svenfuchs", + "name" => "Sven Fuchs", + "email" => "sven@fuchs.com", + "github_id" => user.github_id, + "vcs_id" => user.vcs_id, + "vcs_type" => user.vcs_type, + "avatar_url" => "https://0.gravatar.com/avatar/07fb84848e68b96b69022d333ca8a3e2", + "is_syncing" => user.is_syncing, + "synced_at" => user.synced_at, + "education" => true, + "allow_migration" => false, + "allowance" => { + "@type" => "allowance", + "@representation" => "minimal", + "id" => user.id + }, + "custom_keys" => [], + "recently_signed_up"=>false, + "secure_user_hash" => nil, + "ro_mode" => false, + "confirmed_at" => nil, + 'collaborator' => false + }} + end end diff --git a/spec/v3/services/v2_subscription/update_payment_details_spec.rb b/spec/v3/services/v2_subscription/update_payment_details_spec.rb new file mode 100644 index 0000000000..146311d90a --- /dev/null +++ b/spec/v3/services/v2_subscription/update_payment_details_spec.rb @@ -0,0 +1,90 @@ +describe Travis::API::V3::Services::V2Subscription::UpdatePaymentDetails, set_app: true, billing_spec_helper: true do + let(:billing_url) { 'http://billingfake.travis-ci.com' } + let(:billing_auth_key) { 'secret' } + + before do + Travis.config.billing.url = billing_url + Travis.config.billing.auth_key = billing_auth_key + Travis.config.antifraud.captcha_block_duration = 24 + end + + context 'unauthenticated' do + it 'responds 403' do + patch('/v3/v2_subscription/123/payment_details', { }) + + expect(last_response.status).to eq(403) + end + end + + context 'authenticated' do + let(:user) { FactoryBot.create(:user) } + let(:token) { Travis::Api::App::AccessToken.create(user: user, app_id: 1) } + let(:headers) {{ 'HTTP_AUTHORIZATION' => "token #{token}", + 'CONTENT_TYPE' => 'application/json' }} + let(:address_data) { { + 'address' => 'Rigaer Strasse', + 'first_name' => 'Travis', + 'last_name' => 'Schmidt', + 'company' => 'Travis', + 'city' => 'Berlin', + 'country' => 'Germany', + 'zip_code' => '10001', + 'billing_email' => 'travis@example.org', + 'token' => 'token_from_stripe' + } } + let(:subscription_id) { rand(999) } + + let!(:stubbed_request_address) do + stub_billing_request(:patch, "/v2/subscriptions/#{subscription_id}/address", auth_key: billing_auth_key, user_id: user.id) + .with(body: { + 'first_name' => 'Travis', + 'last_name' => 'Schmidt', + 'company' => 'Travis', + 'address' => 'Rigaer Strasse', + 'city' => 'Berlin', + 'country' => 'Germany', + 'zip_code' => '10001', + 'billing_email' => 'travis@example.org' + }) + .to_return(status: 204) + end + + let!(:stubbed_request_creditcard) do + stub_billing_request(:patch, "/v2/subscriptions/#{subscription_id}/creditcard", auth_key: billing_auth_key, user_id: user.id) + .with(body: { 'token' => 'token_from_stripe', 'fingerprint' => nil }) + .to_return(status: 204) + end + + context 'user is clean' do + before do + Travis.redis.del("recaptcha_attempts_v2_#{subscription_id}") + end + + it 'updates the address and credit card' do + allow_any_instance_of(Travis::API::V3::RecaptchaClient).to receive(:verify).and_return(true) + + patch("/v3/v2_subscription/#{subscription_id}/payment_details", JSON.generate(address_data), headers) + + expect(last_response.status).to eq(204) + expect(stubbed_request_address).to have_been_made.once + expect(stubbed_request_creditcard).to have_been_made.once + end + end + + context 'user failed captcha check' do + before do + Travis.redis.setex("recaptcha_attempts_v2_#{subscription_id}", Travis.config.antifraud.captcha_block_duration, 1) + end + + it 'updates the address and credit card' do + allow_any_instance_of(Travis::API::V3::RecaptchaClient).to receive(:verify).and_return(false) + + patch("/v3/v2_subscription/#{subscription_id}/payment_details", JSON.generate(address_data), headers) + + expect(last_response.status).to eq(400) + expect(stubbed_request_address).to_not have_been_made + expect(stubbed_request_creditcard).to_not have_been_made + end + end + end +end