user upgrades: upgrade to new Stripe checkout system.

This upgrades from the legacy version of Stripe's checkout system to the
new version:

> The legacy version of Checkout presented customers with a modal dialog
> that collected card information, and returned a token or a source to
> your website. In contrast, the new version of Checkout is a smart
> payment page hosted by Stripe that creates payments or subscriptions. It
> supports Apple Pay, Dynamic 3D Secure, and many other features.

Basic overview of the new system:

* We send the user to a checkout page on Stripe.
* Stripe collects payment and sends us a webhook notification when the
  order is complete.
* We receive the webhook notification and upgrade the user.

Docs:

* https://stripe.com/docs/payments/checkout
* https://stripe.com/docs/payments/checkout/migration#client-products
* https://stripe.com/docs/payments/handling-payment-events
* https://stripe.com/docs/payments/checkout/fulfill-orders
This commit is contained in:
evazion
2020-12-23 05:15:08 -06:00
parent c17678d509
commit 7762489d7d
18 changed files with 536 additions and 175 deletions

View File

@@ -1,11 +1,12 @@
class UserUpgradesController < ApplicationController class UserUpgradesController < ApplicationController
helper_method :user helper_method :user
skip_before_action :verify_authenticity_token, only: [:create] respond_to :js, :html
def create def create
if params[:stripeToken] @user_upgrade = UserUpgrade.new(recipient: user, purchaser: CurrentUser.user, level: params[:level].to_i)
create_stripe @checkout = @user_upgrade.create_checkout
end
respond_with(@user_upgrade)
end end
def new def new
@@ -22,38 +23,4 @@ class UserUpgradesController < ApplicationController
CurrentUser.user CurrentUser.user
end end
end end
private
def create_stripe
@user = user
if params[:desc] == "Upgrade to Gold"
level = User::Levels::GOLD
cost = UserUpgrade.gold_price
elsif params[:desc] == "Upgrade to Platinum"
level = User::Levels::PLATINUM
cost = UserUpgrade.platinum_price
elsif params[:desc] == "Upgrade Gold to Platinum" && @user.level == User::Levels::GOLD
level = User::Levels::PLATINUM
cost = UserUpgrade.upgrade_price
else
raise "Invalid desc"
end
begin
charge = Stripe::Charge.create(amount: cost, currency: "usd", source: params[:stripeToken], description: params[:desc])
@user.promote_to!(level, User.system, is_upgrade: true)
flash[:success] = true
rescue Stripe::StripeError => e
DanbooruLogger.log(e)
flash[:error] = e.message
end
if @user == CurrentUser.user
redirect_to user_upgrade_path
else
redirect_to user_upgrade_path(user_id: params[:user_id])
end
end
end end

View File

@@ -0,0 +1,13 @@
class WebhooksController < ApplicationController
skip_forgery_protection only: :receive
rescue_with Stripe::SignatureVerificationError, status: 400
def receive
if params[:source] == "stripe"
UserUpgrade.receive_webhook(request)
head 200
else
head 400
end
end
end

View File

@@ -1,24 +1,4 @@
module UserUpgradesHelper module UserUpgradesHelper
def stripe_button(desc, cost, user)
html = %{
<form action="#{user_upgrade_path}" method="POST" class="stripe">
<input type="hidden" name="authenticity_token" value="#{form_authenticity_token}">
#{hidden_field_tag(:desc, desc)}
#{hidden_field_tag(:user_id, user.id)}
<script
src="https://checkout.stripe.com/checkout.js" class="stripe-button"
data-key="#{Danbooru.config.stripe_publishable_key}"
data-name="#{Danbooru.config.canonical_app_name}"
data-description="#{desc}"
data-label="#{desc}"
data-amount="#{cost}">
</script>
</form>
}
raw(html)
end
def cents_to_usd(cents) def cents_to_usd(cents)
number_to_currency(cents / 100, precision: 0) number_to_currency(cents / 100, precision: 0)
end end

View File

@@ -1,13 +1,145 @@
class UserUpgrade class UserUpgrade
attr_reader :recipient, :purchaser, :level
def self.stripe_publishable_key
Danbooru.config.stripe_publishable_key
end
def self.stripe_webhook_secret
Danbooru.config.stripe_webhook_secret
end
def self.gold_price def self.gold_price
2000 2000
end end
def self.platinum_price def self.platinum_price
4000 2 * gold_price
end end
def self.upgrade_price def self.gold_to_platinum_price
2000 platinum_price - gold_price
end
def initialize(recipient:, purchaser:, level:)
@recipient, @purchaser, @level = recipient, purchaser, level.to_i
end
def upgrade_type
if level == User::Levels::GOLD && recipient.level == User::Levels::MEMBER
:gold_upgrade
elsif level == User::Levels::PLATINUM && recipient.level == User::Levels::MEMBER
:platinum_upgrade
elsif level == User::Levels::PLATINUM && recipient.level == User::Levels::GOLD
:gold_to_platinum_upgrade
else
raise ArgumentError, "Invalid upgrade"
end
end
def upgrade_price
case upgrade_type
when :gold_upgrade
UserUpgrade.gold_price
when :platinum_upgrade
UserUpgrade.platinum_price
when :gold_to_platinum_upgrade
UserUpgrade.gold_to_platinum_price
end
end
def upgrade_description
case upgrade_type
when :gold_upgrade
"Upgrade to Gold"
when :platinum_upgrade
"Upgrade to Platinum"
when :gold_to_platinum_upgrade
"Upgrade Gold to Platinum"
end
end
def is_gift?
recipient != purchaser
end
def process_upgrade!
recipient.with_lock do
upgrade_recipient!
end
end
def upgrade_recipient!
recipient.promote_to!(level, User.system, is_upgrade: true)
end
concerning :StripeMethods do
def create_checkout
Stripe::Checkout::Session.create(
mode: "payment",
success_url: Routes.user_upgrade_url(user_id: recipient.id),
cancel_url: Routes.new_user_upgrade_url(user_id: recipient.id),
client_reference_id: "user_#{purchaser.id}",
customer_email: recipient.email_address&.address,
payment_method_types: ["card"],
line_items: [{
price_data: {
unit_amount: upgrade_price,
currency: "usd",
product_data: {
name: upgrade_description,
},
},
quantity: 1,
}],
metadata: {
purchaser_id: purchaser.id,
recipient_id: recipient.id,
purchaser_name: purchaser.name,
recipient_name: recipient.name,
upgrade_type: upgrade_type,
is_gift: is_gift?,
level: level,
},
)
end
class_methods do
def register_webhook
webhook = Stripe::WebhookEndpoint.create({
url: Routes.webhook_user_upgrade_url(source: "stripe"),
enabled_events: [
"payment_intent.created",
"payment_intent.payment_failed",
"checkout.session.completed",
],
})
webhook.secret
end
def receive_webhook(request)
event = build_event(request)
if event.type == "checkout.session.completed"
checkout_session_completed(event)
end
end
def build_event(request)
payload = request.body.read
signature = request.headers["Stripe-Signature"]
Stripe::Webhook.construct_event(payload, signature, stripe_webhook_secret)
end
def checkout_session_completed(event)
recipient = User.find(event.data.object.metadata.recipient_id)
purchaser = User.find(event.data.object.metadata.purchaser_id)
level = event.data.object.metadata.level
user_upgrade = UserUpgrade.new(recipient: recipient, purchaser: purchaser, level: level)
user_upgrade.process_upgrade!
end
end
end end
end end

View File

@@ -1,10 +0,0 @@
<div class="section">
<p>You can pay with a credit or debit card. Safebooru uses <a href="https://www.stripe.com">Stripe</a> as a payment intermediary so none of your personal information will be stored on the site.</p>
<% if user.level < User::Levels::GOLD %>
<%= stripe_button("Upgrade to Gold", UserUpgrade.gold_price, user) %>
<%= stripe_button("Upgrade to Platinum", UserUpgrade.platinum_price, user) %>
<% elsif user.level < User::Levels::PLATINUM %>
<%= stripe_button("Upgrade Gold to Platinum", UserUpgrade.upgrade_price, user) %>
<% end %>
</div>

View File

@@ -1,3 +0,0 @@
<div class="section">
<p>You can pay with a credit or debit card on <%= link_to "Safebooru", new_user_upgrade_url(host: "safebooru.donmai.us", protocol: "https") %>. Your account will then also be upgraded on Danbooru. You can login to Safebooru with the same username and password you use on Danbooru.</p>
</div>

View File

@@ -0,0 +1,2 @@
var stripe = Stripe("<%= j UserUpgrade.stripe_publishable_key %>");
stripe.redirectToCheckout({ sessionId: "<%= j @checkout.id %>" });

View File

@@ -1,5 +1,6 @@
<% page_title "Account Upgrade" %> <% page_title "Account Upgrade" %>
<% meta_description "Upgrade to a Gold or Platinum account on #{Danbooru.config.app_name}." %> <% meta_description "Upgrade to a Gold or Platinum account." %>
<script src="https://js.stripe.com/v3/"></script>
<%= render "users/secondary_links" %> <%= render "users/secondary_links" %>
@@ -96,9 +97,24 @@
<% if CurrentUser.is_anonymous? %> <% if CurrentUser.is_anonymous? %>
<p><%= link_to "Sign up", new_user_path %> or <%= link_to "login", login_path(url: new_user_upgrade_path) %> first to upgrade your account.</p> <p><%= link_to "Sign up", new_user_path %> or <%= link_to "login", login_path(url: new_user_upgrade_path) %> first to upgrade your account.</p>
<% elsif CurrentUser.safe_mode? %> <% elsif CurrentUser.safe_mode? %>
<%= render "stripe_payment" %> <div class="section">
<p>You can pay with a credit or debit card. Safebooru uses <a href="https://www.stripe.com">Stripe</a>
as a payment intermediary so none of your personal information will be stored on the site.</p>
<% if user.level < User::Levels::GOLD %>
<p><%= button_to "Upgrade to Gold", user_upgrade_path(user_id: user.id, level: User::Levels::GOLD), remote: true, disable_with: "Redirecting..." %></p>
<p><%= button_to "Upgrade to Platinum", user_upgrade_path(user_id: user.id, level: User::Levels::PLATINUM), remote: true, disable_with: "Redirecting..." %></p>
<% elsif user.level < User::Levels::PLATINUM %>
<p><%= button_to "Upgrade Gold to Platinum", user_upgrade_path(user_id: user.id, level: User::Levels::PLATINUM), remote: true, disable_with: "Redirecting..." %></p>
<% end %>
</div>
<% else %> <% else %>
<%= render "stripe_payment_safebooru" %> <div class="section">
<p>You can pay with a credit or debit card on
<%= link_to "Safebooru", new_user_upgrade_url(user_id: user.id, host: "safebooru.donmai.us", protocol: "https") %>.
Your account will then also be upgraded on Danbooru. You can login to
Safebooru with the same username and password you use on Danbooru.</p>
</div>
<% end %> <% end %>
<% end %> <% end %>
</div> </div>

View File

@@ -359,6 +359,9 @@ module Danbooru
def stripe_publishable_key def stripe_publishable_key
end end
def stripe_webhook_secret
end
def twitter_api_key def twitter_api_key
end end

View File

@@ -1 +1,2 @@
Stripe.api_key = Danbooru.config.stripe_secret_key Stripe.api_key = Danbooru.config.stripe_secret_key
Stripe.api_version = "2020-08-27"

View File

@@ -257,6 +257,9 @@ Rails.application.routes.draw do
resource :user_upgrade, :only => [:new, :create, :show] resource :user_upgrade, :only => [:new, :create, :show]
resources :user_feedbacks, except: [:destroy] resources :user_feedbacks, except: [:destroy]
resources :user_name_change_requests, only: [:new, :create, :show, :index] resources :user_name_change_requests, only: [:new, :create, :show, :index]
resources :webhooks do
post :receive, on: :collection
end
resources :wiki_pages, id: /.+?(?=\.json|\.xml|\.html)|.+/ do resources :wiki_pages, id: /.+?(?=\.json|\.xml|\.html)|.+/ do
put :revert, on: :member put :revert, on: :member
get :search, on: :collection get :search, on: :collection

View File

@@ -0,0 +1,55 @@
{
"id": "evt_000",
"object": "event",
"api_version": "2020-08-27",
"created": 1608705740,
"data": {
"object": {
"id": "cs_test_000",
"object": "checkout.session",
"allow_promotion_codes": null,
"amount_subtotal": 2000,
"amount_total": 2000,
"billing_address_collection": null,
"cancel_url": "http://localhost/user_upgrade/new",
"client_reference_id": "user_12345",
"currency": "usd",
"customer": "cus_000",
"customer_email": null,
"livemode": false,
"locale": null,
"metadata": {
"purchaser_id": "12345",
"recipient_id": "12345",
"purchaser_name": "user_12345",
"recipient_name": "user_12345",
"upgrade_type": "gold_upgrade",
"is_gift": "false",
"level": "30"
},
"mode": "payment",
"payment_intent": "pi_000",
"payment_method_types": [
"card"
],
"payment_status": "paid",
"setup_intent": null,
"shipping": null,
"shipping_address_collection": null,
"submit_type": null,
"subscription": null,
"success_url": "http://localhost/user_upgrade?user_id=12345",
"total_details": {
"amount_discount": 0,
"amount_tax": 0
}
}
},
"livemode": false,
"pending_webhooks": 3,
"request": {
"id": null,
"idempotency_key": null
},
"type": "checkout.session.completed"
}

View File

@@ -0,0 +1,67 @@
{
"id": "evt_000",
"object": "event",
"api_version": "2020-08-27",
"created": 1608705945,
"data": {
"object": {
"id": "pi_000",
"object": "payment_intent",
"amount": 2000,
"amount_capturable": 0,
"amount_received": 0,
"application": null,
"application_fee_amount": null,
"canceled_at": null,
"cancellation_reason": null,
"capture_method": "automatic",
"charges": {
"object": "list",
"data": [],
"has_more": false,
"total_count": 0,
"url": "/v1/charges?payment_intent=pi_000"
},
"client_secret": "pi_000",
"confirmation_method": "automatic",
"created": 1608705945,
"currency": "usd",
"customer": null,
"description": null,
"invoice": null,
"last_payment_error": null,
"livemode": false,
"metadata": {},
"next_action": null,
"on_behalf_of": null,
"payment_method": null,
"payment_method_options": {
"card": {
"installments": null,
"network": null,
"request_three_d_secure": "automatic"
}
},
"payment_method_types": [
"card"
],
"receipt_email": null,
"review": null,
"setup_future_usage": null,
"shipping": null,
"source": null,
"statement_descriptor": null,
"statement_descriptor_suffix": null,
"status": "requires_payment_method",
"transfer_data": null,
"transfer_group": null
}
},
"livemode": false,
"pending_webhooks": 3,
"request": {
"id": "req_000",
"idempotency_key": null
},
"type": "payment_intent.created"
}

View File

@@ -1,14 +1,6 @@
require 'test_helper' require 'test_helper'
class UserUpgradesControllerTest < ActionDispatch::IntegrationTest class UserUpgradesControllerTest < ActionDispatch::IntegrationTest
setup do
StripeMock.start
end
teardown do
StripeMock.stop
end
context "The user upgrades controller" do context "The user upgrades controller" do
context "new action" do context "new action" do
should "render" do should "render" do
@@ -25,103 +17,24 @@ class UserUpgradesControllerTest < ActionDispatch::IntegrationTest
end end
context "create action" do context "create action" do
setup do mock_stripe!
@user = create(:user)
@token = StripeMock.generate_card_token
end
context "a self upgrade" do context "for a self upgrade to Gold" do
should "upgrade a Member to Gold" do should "redirect the user to the Stripe checkout page" do
post_auth user_upgrade_path, @user, params: { stripeToken: @token, desc: "Upgrade to Gold" } user = create(:member_user)
post_auth user_upgrade_path(user_id: user.id), user, params: { level: User::Levels::GOLD }, xhr: true
assert_redirected_to user_upgrade_path
assert_equal(true, @user.reload.is_gold?)
end
should "upgrade a Member to Platinum" do
post_auth user_upgrade_path, @user, params: { stripeToken: @token, desc: "Upgrade to Platinum" }
assert_redirected_to user_upgrade_path
assert_equal(true, @user.reload.is_platinum?)
end
should "upgrade a Gold user to Platinum" do
@user.update!(level: User::Levels::GOLD)
post_auth user_upgrade_path, @user, params: { stripeToken: @token, desc: "Upgrade Gold to Platinum" }
assert_redirected_to user_upgrade_path
assert_equal(true, @user.reload.is_platinum?)
end
should "log an account upgrade modaction" do
assert_difference("ModAction.user_account_upgrade.count") do
post_auth user_upgrade_path, @user, params: { stripeToken: @token, desc: "Upgrade to Gold" }
end
end
should "send the user a dmail" do
assert_difference("@user.dmails.received.count") do
post_auth user_upgrade_path, @user, params: { stripeToken: @token, desc: "Upgrade to Gold" }
end
end
end
context "a gifted upgrade" do
should "upgrade the user to Gold" do
@other_user = create(:user)
post_auth user_upgrade_path, @user, params: { stripeToken: @token, desc: "Upgrade to Gold", user_id: @other_user.id }
assert_redirected_to user_upgrade_path(user_id: @other_user.id)
assert_equal(true, @other_user.reload.is_gold?)
assert_equal(false, @user.reload.is_gold?)
end
end
context "an upgrade for a user above Platinum level" do
should "not demote the user" do
@builder = create(:builder_user)
post_auth user_upgrade_path, @user, params: { stripeToken: @token, desc: "Upgrade to Gold", user_id: @builder.id }
assert_response 403
assert_equal(true, @builder.reload.is_builder?)
end
end
context "an upgrade with a missing Stripe token" do
should "not upgrade the user" do
post_auth user_upgrade_path, @user, params: { desc: "Upgrade to Gold" }
assert_response :success assert_response :success
assert_equal(true, @user.reload.is_member?)
end end
end end
context "an upgrade with an invalid Stripe token" do context "for a gifted upgrade to Gold" do
should "not upgrade the user" do should "redirect the user to the Stripe checkout page" do
post_auth user_upgrade_path, @user, params: { stripeToken: "garbage", desc: "Upgrade to Gold" } recipient = create(:member_user)
purchaser = create(:member_user)
post_auth user_upgrade_path(user_id: recipient.id), purchaser, params: { level: User::Levels::GOLD }, xhr: true
assert_redirected_to user_upgrade_path assert_response :success
assert_equal(true, @user.reload.is_member?)
end
end
context "an upgrade with an credit card that is declined" do
should "not upgrade the user" do
StripeMock.prepare_card_error(:card_declined)
post_auth user_upgrade_path, @user, params: { stripeToken: @token, desc: "Upgrade to Gold" }
assert_redirected_to user_upgrade_path
assert_equal(true, @user.reload.is_member?)
end
end
context "an upgrade with an credit card that is expired" do
should "not upgrade the user" do
StripeMock.prepare_card_error(:expired_card)
post_auth user_upgrade_path, @user, params: { stripeToken: @token, desc: "Upgrade to Gold" }
assert_redirected_to user_upgrade_path
assert_equal(true, @user.reload.is_member?)
end end
end end
end end

View File

@@ -0,0 +1,167 @@
require 'test_helper'
class WebhooksControllerTest < ActionDispatch::IntegrationTest
mock_stripe!
def post_webhook(*args, **metadata)
event = StripeMock.mock_webhook_event(*args, metadata: metadata)
signature = generate_stripe_signature(event)
headers = { "Stripe-Signature": signature }
post receive_webhooks_path(source: "stripe"), headers: headers, params: event, as: :json
end
# https://github.com/stripe-ruby-mock/stripe-ruby-mock/issues/467#issuecomment-634674913
# https://stripe.com/docs/webhooks/signatures
def generate_stripe_signature(event)
time = Time.now
secret = UserUpgrade.stripe_webhook_secret
signature = Stripe::Webhook::Signature.compute_signature(time, event.to_json, secret)
Stripe::Webhook::Signature.generate_header(time, signature, scheme: Stripe::Webhook::Signature::EXPECTED_SCHEME)
end
context "The webhooks controller" do
context "receive action" do
context "for a request from an unrecognized source" do
should "fail" do
post receive_webhooks_path(source: "blah")
assert_response 400
end
end
context "for a Stripe webhook" do
context "with a missing signature" do
should "fail" do
event = StripeMock.mock_webhook_event("payment_intent.created")
post receive_webhooks_path(source: "stripe"), params: event, as: :json
assert_response 400
end
end
context "with an invalid signature" do
should "fail" do
event = StripeMock.mock_webhook_event("payment_intent.created")
headers = { "Stripe-Signature": "blah" }
post receive_webhooks_path(source: "stripe"), headers: headers, params: event, as: :json
assert_response 400
end
end
context "for a payment_intent.created event" do
should "work" do
post_webhook("payment_intent.created")
assert_response 200
end
end
context "for a checkout.session.completed event" do
context "for a self upgrade" do
context "of a Member to Gold" do
should "upgrade the user" do
@user = create(:member_user)
post_webhook("checkout.session.completed", {
recipient_id: @user.id,
purchaser_id: @user.id,
upgrade_type: "gold_upgrade",
level: User::Levels::GOLD,
})
assert_response 200
assert_equal(User::Levels::GOLD, @user.reload.level)
end
end
context "of a Member to Platinum" do
should "upgrade the user" do
@user = create(:member_user)
post_webhook("checkout.session.completed", {
recipient_id: @user.id,
purchaser_id: @user.id,
upgrade_type: "platinum_upgrade",
level: User::Levels::PLATINUM,
})
assert_response 200
assert_equal(User::Levels::PLATINUM, @user.reload.level)
end
end
context "of a Gold user to Platinum" do
should "upgrade the user" do
@user = create(:gold_user)
post_webhook("checkout.session.completed", {
recipient_id: @user.id,
purchaser_id: @user.id,
upgrade_type: "gold_to_platinum_upgrade",
level: User::Levels::PLATINUM,
})
assert_response 200
assert_equal(User::Levels::PLATINUM, @user.reload.level)
end
end
end
context "for a gifted upgrade" do
context "of a Member to Gold" do
should "upgrade the user" do
@recipient = create(:member_user)
@purchaser = create(:member_user)
post_webhook("checkout.session.completed", {
recipient_id: @recipient.id,
purchaser_id: @purchaser.id,
upgrade_type: "gold_upgrade",
level: User::Levels::GOLD,
})
assert_response 200
assert_equal(User::Levels::GOLD, @recipient.reload.level)
end
end
context "of a Member to Platinum" do
should "upgrade the user" do
@recipient = create(:member_user)
@purchaser = create(:member_user)
post_webhook("checkout.session.completed", {
recipient_id: @recipient.id,
purchaser_id: @purchaser.id,
upgrade_type: "platinum_upgrade",
level: User::Levels::PLATINUM,
})
assert_response 200
assert_equal(User::Levels::PLATINUM, @recipient.reload.level)
end
end
context "of a Gold user to Platinum" do
should "upgrade the user" do
@recipient = create(:gold_user)
@purchaser = create(:member_user)
post_webhook("checkout.session.completed", {
recipient_id: @recipient.id,
purchaser_id: @purchaser.id,
upgrade_type: "gold_to_platinum_upgrade",
level: User::Levels::PLATINUM,
})
assert_response 200
assert_equal(User::Levels::PLATINUM, @recipient.reload.level)
end
end
end
end
end
end
end
end

View File

@@ -26,6 +26,7 @@ class ActiveSupport::TestCase
include DownloadTestHelper include DownloadTestHelper
include IqdbTestHelper include IqdbTestHelper
include UploadTestHelper include UploadTestHelper
extend StripeTestHelper
mock_post_version_service! mock_post_version_service!
mock_pool_version_service! mock_pool_version_service!

View File

@@ -0,0 +1,13 @@
StripeMock.webhook_fixture_path = "test/fixtures/stripe-webhooks"
module StripeTestHelper
def mock_stripe!
setup do
StripeMock.start
end
teardown do
StripeMock.stop
end
end
end

View File

@@ -0,0 +1,41 @@
require 'test_helper'
class UserUpgradeTest < ActiveSupport::TestCase
context "UserUpgrade:" do
context "the #process_upgrade! method" do
setup do
@user = create(:user)
@user_upgrade = UserUpgrade.new(recipient: @user, purchaser: @user, level: User::Levels::GOLD)
end
should "update the user's level" do
@user_upgrade.process_upgrade!
assert_equal(User::Levels::GOLD, @user.reload.level)
end
should "log an account upgrade modaction" do
assert_difference("ModAction.user_account_upgrade.count") do
@user_upgrade.process_upgrade!
end
end
should "send the user a dmail" do
assert_difference("@user.dmails.received.count") do
@user_upgrade.process_upgrade!
end
end
context "for an upgrade for a user above Platinum level" do
should "not demote the user" do
@user.update!(level: User::Levels::BUILDER)
assert_raise(User::PrivilegeError) do
@user_upgrade.process_upgrade!
end
assert_equal(true, @user.reload.is_builder?)
end
end
end
end
end