I'm writing a system test for an app that uses active storage. The view I'm testing includes presigned urls to objects on amazon s3. I'd obviously like to avoid sending GET requests to s3 in my tests. Instead, I'd like to "stub" active storage so that the urls for my attachments all point to one particular file on my test machine ("/test_data/placeholder.jpg"), rather than to s3.
My view code uses url_for to generate s3 urls. How can I stub url_for to return my one specific desired url?
Excerpts of my failed attempt follow.
In my view (user.jbuilder.json):
json.user_thumbnail = url_for #user.photo.variant(resize: "100x100")
In my tests:
class UserPageTest < ApplicationSystemTestCase
test "thumbnail appears on user page" do
User.any_instance.stubs(:photo).returns(stub(:url => "/test_data/placeholder_thumbnail.jpg") ))
visit '/user_profile/123'
end
end
When I run the test I get this error:
2021-11-04 20:26:40 -0400 Rack app ("GET /user/123.json" - (127.0.0.1)): #<Minitest::Assertion: unexpected invocation: #<Mock:0x7ff448c52af8>.to_model()
In the end I accomplished this by monkey patching activestorage inside my tests. I redirect to different placeholder files depending on the class of the object I'm dealing with.
ActiveStorage::Blobs::RedirectController.class_eval do
def show
case #blob.attachments.first.record.class.name
when "Video"
redirect_to "/test_data/video.mp4"
when "Photo"
redirect_to "/test_data/photo.png"
else
raise StandardError.new "unhandled redirection in ActiveStorage::Blobs::RedirectController monkey patch"
end
end
# include ActiveStorage::SetBlob
# def show
# expires_in ActiveStorage.service_urls_expire_in
# redirect_to #blob.url(disposition: params[:disposition])
# end
end
ActiveStorage::Representations::RedirectController.class_eval do
def show
case #blob.attachments.first.record.class.name
when "Video"
redirect_to "/test_data/video.mp4"
when "Photo"
redirect_to "/test_data/photo.png"
else
raise StandardError.new "unhandled redirection in ActiveStorage::Blobs::RedirectController monkey patch"
end
end
end
Related
This is my task
1) Create a new route localhost:3000/device
2) Consider if this url is hit from the mobile phones or desktop browsers, then
3) Track the system/device (iOS, android, web) from which the URL is hit
4) Based on the device from which the request has come, we need to redirect to some other URL (e.g., iOS ——> “iOS app store”, android ——> “Android play store”, web ——> “google page”)
5) Find what are the different approaches that are available to track the system from which the request has come and what would be the best one to implement and why?
Here I found a solution, but in rspec it causes error.
This is my route
get :devise, to: 'topics#devise'
And this is my controller
class TopicsController < ApplicationController
def devise
if request.env['HTTP_USER_AGENT'].downcase.match(/mac/i)
redirect_to 'https://itunes.apple.com/us/app/apple-store/id375380948?mt=8'
elsif request.env['HTTP_USER_AGENT'].downcase.match(/windows/i)
redirect_to 'https://www.microsoft.com/en-in/store/apps/windows'
elsif request.env['HTTP_USER_AGENT'].downcase.match(/android/i)
redirect_to 'https://play.google.com/store?hl=en'
else
redirect_to root_path
end
end
end
When I hit url lvh.me:3000/devise it redirects to the respective app store.
This is my controller spec
context 'devise' do
it 'should detect the device' do
get :devise
response.should redirect_to '/https://www.microsoft.com/en-in/store/apps/windows'
end
end
and this caused the error:
Expected response to be a redirect to http://test.host/https://www.microsoft.com/en-in/store/apps/windows but was a redirect to http://test.host/.
Expected "http://test.host/https://www.microsoft.com/en-in/store/apps/windows" to be === "http://test.host/".
If I did it in a wrong way, tell some suggestion for doing rspec
If your rails version is not too ancient in controller you can use request.user_agent (it will look into env anyway, but this makes code cleaner)
Browsers pass user agent in header User-agent (which in turn ends up in rack env), so you need to simulate this in your tests.
For testing this I'd recommend using request specs instead of controller ones (which are deprecated in rails 5):
RSpec.describe 'Topics...', type: :request do
it "redirects for ios" do
get '/your/topcis/path/here', headers: { 'HTTP_USER_AGENT' => 'iPhone' }
expect(response).to redirect_to(/\.apple\.com/)
end
end
(above uses rails 5, for older rails headers will be just hash, not a keyword argument)
Also you can write your method with case statement:
def devise
redirect_to case request.user_agent.downcase
when /mac/i then 'https://itunes.apple.com/us/app/apple-store/id375380948?mt=8'
when /windows/i then 'https://www.microsoft.com/en-in/store/apps/windows'
when /android/i then 'https://play.google.com/store?hl=en'
else
root_path
end
end
I'm using service which sends Webhooks to my application. I want to write RSpec test for handling them. It's important to have this request exactly the same (remote caller IP, headers with encrypted content).
I tried to save request as json:
class WebhookController < ApplicationController
def some_callback
File.open('temp/request_example.json','w') do |f|
f.write request.to_json
end
end
end
so I could later do:
describe WebhookController do
subject { get :some_callback, JSON.parse(File.open('temp/request_example.json')) }
it 'does something' do;end
end
but unfortunately you cannot call request.to_json(request.to_json
IOError: not opened for reading). You can't either get directly to request.body or request.headers.
How to save such request for later usage in tests? Is there any gem for it?
Some third-party service which I want to use requires user to log in on their webpage. Handy url is generated in controller below. User goes there and come back to my service after his authentication succeeds
class MyController < App::BaseController
def login
redirect_to SOME_API.external_url(ENV['secret_id'])
end
end
As user comes back to my service he brings some feedback from third-party service in URL params (like: myapp.com/login_callack?response=error&reason=wrong_password). There are many variants of these params so I need to handle them in my action
def login_callback
#SOME MAGIC WITH COOKIES/ERRORS/What ever
redirect_to root_path
end
I want to write feature specs for it using RSpec and capybara.
scenario 'User authenticates with third-party thing' do
visit '/anything'
click_link 'Go to MyController#login and than get redirect to somerandom.url/login_logic'
# use cassette below
fill_out_form
click_button confirm
# magic happens on their side and their redirect user to my app into #login_callback action
expect(page).to have(/all fancy things i need/)
end
end
however calling WebMock.disable_net_connect!(allow_localhost: true) or adding vcr: { cassette_name: 'using_third_party_login', record: :new_episodes } doesn't prevent this scenario to being redirect to external url.
Capybara just let being redirected to external uri and no cassets are being recorded/played. Any idea how to stub redirect_to external_url with cassette play/record?
There are many solutions for creating customized error handling pages, but almost none for Rails 4:
Basic Rails 404 Error Page
Dynamic error pages in Rails
The standard answer of encouraging people to modify 404.html in /public doesn't work for me because I want to use the CSS theme that resides in the asset pipeline. Is there a way that html files can access those styles defined in the asset pipeline? If not, is there a way to create a custom error handler that has access to the pipeline?
For Rails 4.1 I like this answer, add an asset type better; however I have not tried it. On Rails 4.0.8, these three references helped me:
Dynamic error pages is the second reference in the question. This worked just fine for me.
Custom error pages may have cribbed from the first reference, or the other way around, but goes the extra mile by adding some information about testing with Capybara.
I did not do the Capybara testing because I didn't want to change the test configuration; however, RSpec-Rails Request Specs clued me in to test these requests independently and see that they complete and return the correct content.
What follows is a nutshell description of what is taught by the three references:
Add the following setting to config/environments/production.rb
# Route exceptions to the application router vs. default
config.exceptions_app = self.routes
Edit the routing configuration, config/routes.rb to direct the error pages to an errors controller
# error pages
%w( 404 422 500 503 ).each do |code|
get code, :to => "errors#show", :code => code
end
will route the 404, 422, 500, and 503 page requests to the show action of the errors controller with a parameter code that has the value of the status code.
Create the controller, app/controllers/errors_controller.rb. Here is the entire content:
class ErrorsController < ApplicationController
def show
status_code = params[:code] || 500
flash.alert = "Status #{status_code}"
render status_code.to_s, status: status_code
end
end
My preference was to set a status message on flash.alert
Create the pages themselves. I use .erb Here is app/views/errors/500.html.erb
<p>Our apology. Your request caused an error.</p>
<%= render 'product_description' %>
So you see that you can render a partial. The page renders with all of the layout boilerplate from app/views/layouts/application.html.erb or any other layout boilerplate that you have configured. That includes the <div id='alert'><%= alert %></div> that displays the status message from the flash.
Tested with RSpec by adding a test file, spec/requests/errors_request_spec.rb. Here is abbreviated content of that file that shows a test of the 500 status page:
require 'rails_helper'
RSpec.describe "errors", :type => :request do
it "displays the 500 page" do
get "/500"
assert_select 'div#alert', 'Status 500'
assert_select 'div[itemtype]'
end
end
The first assertion checks for the flash alert. The second assertion checks for the partial.
We've made a gem which does this for you: exception_handler.
There is also a great tutorial here.
I also wrote an extensive answer on the subject here.
Middleware
# config/application.rb
config.exceptions_app = ->(env) { ExceptionController.action(:show).call(env) }
Controller
# app/controllers/exception_controller.rb
class ExceptionController < ApplicationController
respond_to :json, :js, :html
before_action :set_status
def show
respond_with #status
end
private
def set_status
def status
#exception = env['action_dispatch.exception']
#status = ActionDispatch::ExceptionWrapper.new(env, #exception).status_code
#response = ActionDispatch::ExceptionWrapper.rescue_responses[#exception.class.name]
end
end
end
View
# app/views/exception/show.html.erb
<h1>404 error</h1>
This is very simple version - I can explain more if you wish.
Basically, you need to hook into the config.exceptions_app middleware, it will capture any exception in the middleware stack (as opposed to rendering the entire environment), allowing you to send the request to your own controller#action.
If you comment, I'll help you out some more if you want!
I have a method in my application that finds a photo from the og:image tag of a link that is submitted. In my create action, I use the method photo_form_url, described below.
def photo_from_url(url)
if !Nokogiri::HTML(open(url)).css("meta[property='og:image']").blank?
photo_url = Nokogiri::HTML(open(url)).css("meta[property='og:image']").first.attributes["content"]
self.photo = URI.parse(photo_url)
self.save
end
end
However, this produces an error if a bad url is entered.
I tried to rescue as below, but this gives me an "undefined method redirect_to"
def photo_from_url(url)
begin
if !Nokogiri::HTML(open(url)).css("meta[property='og:image']").blank?
photo_url = Nokogiri::HTML(open(url)).css("meta[property='og:image']").first.attributes["content"]
self.photo = URI.parse(photo_url)
self.save
end
rescue OpenURI::HTTPError
redirect_to :back, notice: 'link's broken!'
end
end
What am I doing wrong?
According to your answer to my comment, your function photo_from_url is defined in the model. Trying to redirect a user within a model is not possible as shown by the error you are facing.
Bear in mind that your model can be called outside of a browsing session environment. EG:
tests
rake task
You should thus never, ever put any code that has to do with manipulating the user browser, or the user session within your models. This is the job of the controller.
So what you need to do is simply raise an exception or return a specific value in your model when you are encountering a bad url. And react to that exception / return value in your controller by redirecting the user. This ensure that anything that has to do with the user browser stays in the controller, and that you could implement a different behavior in a rake task if encountering the same error.
So, your model should do stuff, and raise errors when it can't :
# Link.rb
def photo_from_url(url)
if !Nokogiri::HTML(open(url)).css("meta[property='og:image']").blank?
photo_url = Nokogiri::HTML(open(url)).css("meta[property='og:image']").first.attributes["content"]
self.photo = URI.parse(photo_url)
self.save
end
end
Your controller should ask your model to do stuff, and deal with the user if there is a problem :
# link_controller
# in create
begin
link.photo_from_url(url)
rescue OpenURI::HTTPError
redirect_to :back, notice: 'link's broken!'
end