This is my controller
class Api::V1::WebhooksController < ApplicationController
include Api::V1::WebhookHelper
include Api::V1::LogHelper
skip_before_action :verify_authenticity_token
after_action :handle_wehbook
# Schedule plan
def schedule_plan
add_log(status: "webhook", message: "New Plan is scheduled.")
end
def handle_wehbook
if webhook_verified?
webhook_verified!
render status: 200, json: { error: 'webhook is verified.' }
else
webhook_verified!(verified: false)
render status: 500, json: { error: 'webhook is not verified.' }
end
end
end
This is Webhook Helper.
I am sure in WebhookHelper, it never redirects or renders anything.
require 'openssl'
require 'base64'
module Api::V1::WebhookHelper
include Api::V1::LogHelper
def webhook_verified?
digest = OpenSSL::Digest.new('sha256')
hmac = OpenSSL::HMAC.digest(digest, secret, request.body.read)
hash = Base64.encode64(hmac).strip
hash == signature
end
def secret
ENV["API_KEY"]
end
def signature
request.headers["HTTP_X_SIGNATURE"]
end
def webhook_verified!(verified: true)
if verified
add_log(status: "webhook", message: "Webhook is verified.") # only puts log
else
add_log(status: "webhook", type: "warning", message: "Webhook is not verified.") # only puts log
end
end
end
I am getting this issue.
AbstractController::DoubleRenderError (Render and/or redirect were called multiple times in this action. Please note that you may only call render OR redirect, and at most once per action. Also note that neither redirect nor render terminate execution of the action, so if you want to exit an action after redirecting, you need to do something like "redirect_to(...) and return".):
app/controllers/api/v1/webhooks_controller.rb:31:in `handle_wehbook'
I am not sure I am calling render or redirect multiple times in my action.
Anyone can help me?
Your handle_wehbook function is an after_action, it runs after some other action.
The latter has already rendered something (it may be an rails error or a redirect) thus the double render
Related
I am trying to write the allow method in RSpec. My rails controller is
module Users
class ProfilesController < ApplicationController
# Update user profile
def update
payload = { name: params[:user][:name],email: params[:user][:email]}
response = send_request_to_update_in_company(payload)
if response['code'] == 200
if User.first.update(user_params)
render json: { message: "User successfully updated"}, status: :ok
else
head :unprocessable_entity
end
else
render json: { error: 'Error updating user in Company' },status: :unprocessable_entity
end
end
private
def send_request_to_update_in_comapny(payload)
response = Api::V1::CompanyRequestService.new(
payload: payload.merge(company_api_access_details),
url: 'customers/update_name_email',
request_method: Net::HTTP::Post
).call
JSON.parse(response.body)
end
end
end
When I write the bellow code in my test file
allow(Users::ProfilesController).to receive(:send_request_to_update_in_company).and_return({ 'code' => 500 })
I am getting the following error in terminal
Users::ProfilesController does not implement: send_request_to_update_in_comapny
enter code here
With allow_any_instance_of I am able to get the code working. But how can I implement it using allow?
Yes, allow_any_instance_of works because, as the name suggests, it allows any instance of Users::ProfilesController to respond to the instance method send_request_to_update_in_company with your mock return value.
However, your line
allow(Users::ProfilesController).to receive(:send_request_to_update_in_company)
is telling RSpec to mock a class method called send_request_to_update_in_company, which doesn't exist. And so, you're seeing the error message saying so.
You don't say where your test is situated, but generally wherever it is, it's not a good idea to either test or stub out a private method.
I'd be inclined to instead create a mock Api::V1::CompanyRequestService object to return a fake response, which your controller code can then parse as expected and produce the expected JSON. For example
mock_request = instance_double(Api::V1::CompanyRequestService)
allow(mock_request).to receive(:call).and_return('{"code": 500}')
allow(Api::V1::CompanyRequestService).to receive(:new).and_return(mock_request)
Another approach might be to leave your service alone, and instead use tools like VCR or WebMock to provide mocked JSON values at the network layer - your code can think it's calling out to the internet, but really it gets back responses that you define in your tests.
How about this way:
spec/requests/users/profiles_controller_spec.rb
require 'rails_helper'
RSpec.describe "Users::ProfilesControllers", type: :request do
describe "Test call to special function: " do
let(:controller) { Users::ProfilesController.new }
it "Should response to code 500" do
response = controller.send_request_to_update_in_company("test")
expect(response).to eq({"code"=>"500", "test1"=>"abc", "test2"=>"def"})
end
it "Should return to true" do
response = controller.true_flag?
expect(response).to eq(true)
end
end
end
app/controllers/users/profiles_controller.rb
module Users
class ProfilesController < ApplicationController
# Update user profile
def update
payload = { name: params[:user][:name],email: params[:user][:email]}
response = send_request_to_update_in_company(payload)
Rails.logger.debug "Ok71 = response['code'] = #{response['code']}"
# if response['code'] == 200
# if User.first.update(user_params)
# render json: { message: "User successfully updated"}, status: :ok
# else
# head :unprocessable_entity
# end
# else
# render json: { error: 'Error updating user in Company' },status: :unprocessable_entity
# end
end
# Not private, and not mistake to 'send_request_to_update_in_comapny'
def send_request_to_update_in_company(payload)
response = Api::V1::CompanyRequestService.new(
payload: "for_simple_payload_merge_values",
url: 'for_simple_customers/update_name_email',
request_method: "for_simple_request_method"
).call
Rails.logger.debug "Ok66 = Start to log response"
Rails.logger.debug response
JSON.parse(response.body)
end
# Simple function to test
def true_flag?
true
end
end
end
app/services/api/v1/company_request_service.rb
class Api::V1::CompanyRequestService < ActionController::API
def initialize(payload="test1", url="test2", request_method="test3")
#payload = payload
#url = url
#request_method = request_method
end
def call
#object = Example.new
#object.body = {code: "500", test1: "abc", test2: "def"}.to_json
return #object
end
end
class Example
attr_accessor :body
def initialize(body={code: "000", test1: "init_value_abc", test2: "init_value_def"}.to_json)
#body = body
end
end
I use simple code to simulate your project. Modify it to suitable your working! Tell me about your its thinking. Thank you!
I am using Mailchimp (via the Gibbon gem) to add email addresses to my Mailchimp mailing list, and I want to handle any errors that are returned by Mailchimp and display them in my view.
Here is my Pages controller:
class PagesController < ApplicationController
def subscribe
email = subscriber_params[:email]
if email.empty?
flash[:error] = 'Please provide an email.'
redirect_to root_path
else
subscriber = Mailchimp.new.upsert(email)
if subscriber
flash[:success] = 'You\'re in!'
redirect_to root_path(subscribed: :true)
else
# Return error coming from Mailchimp (i.e. Gibbon::MailChimpError)
end
end
end
end
And here is the app > services > mailchimp.rb file I set up to separate out the Mailchimp logic:
class Mailchimp
def initialize
#gibbon = Gibbon::Request.new(api_key: Rails.application.credentials.mailchimp[:api_key])
#list_id = Rails.application.credentials.mailchimp[:list_id]
end
def upsert(email)
begin
#gibbon.lists(#list_id).members.create(
body: {
email_address: email,
status: "subscribed"
}
)
rescue Gibbon::MailChimpError => e #This is at the bottom of the Gibbon README
raise e.detail
end
end
end
What I'm trying to figure out is how to return/send Gibbon::MailChimpError back to my Pages#subscribe action. I see it being outputted as a RuntimeError in my console, but I'm not sure the right way to access/pass it along.
And please let me know if there's a better practice for this kind of implementation.
You could move the begin/rescue block to the subscribe action inside your controller to handle the error from there, or even better, you can use rescue_from in your controller like this:
class PagesController < ApplicationController
rescue_from Gibbon::MailChimpError do |e|
# Handle the exception however you want
end
def subscribe
# ....
end
end
I have this class:
class SuperAppController < ApplicationController
layout 'app'
add_flash_types :error, :info
skip_before_action :verify_authenticity_token
after_action :check_account!
before_filter do
if current_empresa != nil
authenticate_empresa!
else
authenticate_user!
end
end
private
def check_account!
if empresa_signed_in? && current_empresa.data_pagamento.blank? ||
empresa_signed_in? && current_empresa.data_pagamento && Time.zone.now > current_empresa.data_pagamento + 1.year
redirect_to pagamento_index_path, notice: 'Active your account' and return
end
end
end
I use the function "check_account" to see if the account of user was expired.
When the action is loaded, I'm getting this error:
Render and/or redirect were called multiple times in this action.
Please note that you may only call render OR redirect, and at most
once per action. Also note that neither redirect nor render terminate
execution of the action, so if you want to exit an action after
redirecting, you need to do something like "redirect_to(...) and
return".
When I do before_action instead of after_action I get the same error, but the view of the error is not loaded. I get e message from browser saying: "Localhost is not working". And I can see a lot of requests logs on terminal.
Someone know why I'm geting so many redirects?
thanks!
For example...
module Api
module V1
class SessionsController < ApplicationController
respond_to :json
skip_before_filter :verify_authenticity_token
def create
#user = User.find_by(email: params[:session][:email].downcase)
if #user && #user.authenticate(params[:session][:password])
token = User.new_remember_token
#user.update_attribute(:remember_token, User.digest(token))
respond_with :api, :v1, _____________
else
#error
end
end
end
end
end
The #error part of the code, if the user is not properly authenticated. What syntax do I need to properly convey to the caller that the authentication did not go through for example, or in other cases, maybe data was not saved?
Like CBroe said, respond with an appropriate status code, such as 400 or 403. You could do just that (using 'head' to return the status code only), or also add an error message in JSON format:
{ 'error' : 'Authorization failed' }
The client code will want to check the status code and possibly the 'error' key in the JSON response and handle it appropriately.
Examples to put at the end of your controller action (pick one):
return head(:bad_request) # returns a 400 status code only
render :json => { :error => 'That was an invalid request' } # defaults to 200 status
render :json => { :error => 'Oops! Bad request' }, :status => 400
The last example overrides the default status to make it a 400. In general, the status can be an integer like that, or a symbol like :not_found or :bad_request. Hope that helps.
The performed? method returns true in a controller if the application has redirected or rendered.
How can I determine whether the application rendered (rather than redirected)?
Judging from the performed? docs, I can look at #performed_render, but this seems hacky.
Edit: Why do I want to do this?
I'm trying to cache the output of a controller action only in the case that it results in a render (and not a redirect). Here's the code I'm using now: (based on render-caching)
def render_with_cache(options = {})
key = ([request.request_uri, request.format] | (options.delete(:key_additions) || [])).join("::")
body = Rails.cache.read(key)
if body
render :text => body
else
yield if block_given?
render unless performed?
if #performed_render
Rails.cache.write(key, response.body, options) # :expires_in
end
end
end
Rails 3.2+ now uses a method called performed? for determining whether or not a render or redirect has already been performed.
Contrived example:
def index
redirect_to not_found_path unless authenticated?
render action: "show_all" unless performed?
end
Look at #performed_render. :) You don't explain why you need to detect this, so I am unable to suggest alternative solutions.
In an after filter, check the codes for the response status .
MyController < ActionController
after_filter :check_response
# define your actions that render or redirect
protected
def check_response
# put real app logic here
puts :rendered! if response.status == "200 OK"
puts :redirected! if response.status == "302 Found"
end
end