ActionMailer deliver_later works in browser but not in test suite - ruby-on-rails

I'm in the process of porting most of my mailers in my Rails 5 app to use deliver_later instead of deliver_now.
One of my mailers works just fine when I test it in the browser, but not when I run it in an integration test. (I've even started my rails server using my test environment and tested in the browser using the exact same data as my integration test, and it still runs fine in the browser, but the integration test fails).
The integration test is failing with the error:
ERROR -- : Error performing ActionMailer::DeliveryJob (Job ID:
19b8c1c9-81e1-4209-b97b-0feb1e22fad3) from Async(mailers) in 65.64ms:
ActionView::Template::Error (undefined method `message_text' for
nil:NilClass):
The line in my email template triggering the error is:
<p><%= simple_format(#message.message_text )%></p>
I'm completely stumped figuring out why my #message object is nil when the mailer is called from an integration test but not in the browser.
Relevant code:
jobs_controller:
cj = ContactJob.create!(job_id: #job.id, contact_id: id, message:message)
...
cj.send_job_email
contact_job.rb:
class ContactJob < ApplicationRecord
belongs_to :message
belongs_to :contact
belongs_to :job
validates :contact, uniqueness: {scope: :job}
validates :message, presence:true
def send_job_email()
ContactJobMailer.job(self.id).deliver_later
end
end
contact_job_mailer.rb:
def job (contact_job_id)
#cj = ContactJob.find(contact_job_id)
#contact=#cj.contact
#job= #cj.job
#message= #cj.message
mail to: #contact.email, subject: 'test'
end
integration test:
patch update_contacts_user_job_path(#user, #job), params: {job: {contact_ids: [ #contact2.id], message: {message_text: "This is a test"}}}
It's only the #message object that is nil, and only when I run it as part of an integration test, everything works fine in the browser.

Related

Rails system test w/ Capybara RackTest raises ActiveSupport::MessageVerifier::InvalidSignature

I have a typical Rails model form with a file attachment selector allowing multiple attachments. It works fine in development, but during a system test, raises an ActiveSupport::MessageVerifier::InvalidSignature exception.
Rails 7.0.2.2
capybara 3.36.0
rack-test 1.1.0
The model has_many_attached :photos.
The form is using form_with and multipart: true.
The HTML source looks correct.
In development, manually using the form with 0 or any file attachments works as expected.
In my system test, I am using the rack_test driver.
test "creating a quote request" do
visit new_quote_request_path
fill_in "First name", with: 'FAKE FIRST'
# ...
click_on "Submit"
assert_text "Success"
end
In the controller, my canonical param-permitting method looks like:
def quote_request_params
params.require(:quote_request).permit(:first_name, :last_name, :email,
:phone_number, :shipping, :promo_code, :description, :item_type_id, :brand_id,
photos: [])
end
My controller create method is typical...
def create
#quote_request = QuoteRequest.new(quote_request_params)
respond_to do |format|
# ...
In the system test, the call of QuoteRequest.new(quote_request_params) raises an ActiveSupport::MessageVerifier::InvalidSignature exception.
With a breakpoint in place, I can see that the quote_request_params looks like:
#<ActionController::Parameters {"first_name"=>"FAKE FIRST",
"last_name"=>"FAKE LAST", "email"=>"fake#fake.com",
"phone_number"=>"5415555555", "shipping"=>"1", "promo_code"=>"",
"description"=>"Fake quote request description.",
"item_type_id"=>"980190962", "brand_id"=>"980190962",
"photos"=>[
"",
"#<Capybara::RackTest::Form::NilUploadedFile:0x000000010dae35b8>"
]} permitted: true>
And it seems Capybara is doing its default behavior of attaching a 'nil file' for the multipart form.
Why is the test raising an ActiveSupport::MessageVerifier::InvalidSignature exception?
This is an issue with Rails 7 and rack-test. It can temporarily be solved by putting the following in config/environments/test.rb:
config.active_storage.multiple_file_field_include_hidden = false
Refer to the rack-test issue for more details.

Cannot bypass Omniauth initializer in test mode

I am trying to test my graphql schema without any need for authentication.
I have added skip_before_action :verify_authenticity_token to the GraphqlController, and when using postman (copied by a curl request from graphiql), and I am seeing a successful query in development mode.
In postman I have the query in the body {"query":"{\n user(id: 1) {\n id\n created_at\n updated_at\n jwt\n}\n}\n","variables":null,"operationName":null}, and Content-Type application/json in the header and this works fine.
Now in test mode, I am hitting the auth initializer for Omniauth:
Rails.application.config.middleware.use OmniAuth::Builder do
provider(
:auth0,
Auth0::Config["app_client_id"],
Auth0::Config["app_client_secret"],
Auth0::Config["domain"],
callback_path: "/auth/auth0/callback"
)
end
Although I don't want to since I don't want any headers required in this post request.
Here is my rspec request:
require 'graphlient'
RSpec.shared_context "GraphQL Client", shared_context: :metadata do
let(:client) do
Graphlient::Client.new('https://api.example.org/graphql') do |client|
client.http do |h|
h.connection do |c|
c.use Faraday::Adapter::Rack, app
end
end
end
end
end
and here's the actual test
it 'retrieves schema' do
expect(client.schema).to be_a GraphQL::Schema
end
with the error:
Failure/Error:
expect { client.schema.status }
.to raise_error(Graphlient::Errors::ServerError)
expected Graphlient::Errors::ServerError, got #<ArgumentError: Received wrong number of arguments. [nil, nil, nil, {:callback_path=>"/auth/auth0/callback"}]> with backtrace:
# /usr/local/bundle/gems/omniauth-auth0-1.4.2/lib/omniauth/strategies/auth0.rb:41:in `initialize'
I think I got it! I forgot to add keys for test in config/auth0.yml.
It was a hidden file.

Weird error when testing a Rails + AngularJS app with Rspec and PhantomJS

I have an app that lists tickets. It uses AngularJS. Here's the controller action:
def index
#tickets = apply_scopes(#tickets)
response.headers['x-total-count'] = #tickets.total_count
response.headers['x-per-page'] = Ticket.default_per_page
end
The Angular controller (Coffeescript):
$scope.fetch = ->
Ticket.query($scope.search).then (response) ->
$scope.tickets = response.data
$scope.totalCount = response.headers('x-total-count')
$scope.perPage = response.headers('x-per-page')
$scope.fetch()
I'm using angular-rails-resource to fetch the records. Everything works smoothly if I test by hand.
Here is the spec:
let(:user) { create :user }
scenario 'User lists tickets', js: true do
login_as user, scope: :user
ticket = create :ticket, user: user
visit root_path
click_on 'Support Requests'
expect(page).to have_content(ticket.subject)
end
When I run this spec, I just get the regular Rspec failure message because the condition was not met, but it should have:
expected to find text "ticket 000" in...
I figured it had something to do with concurrency and Capybara not waiting for Angular to fetch and display the records. Then I went ahead and added a sleep 2 right above the expectation just to test that. When I do it, I get a different error:
Capybara::Poltergeist::JavascriptError:
One or more errors were raised in the Javascript code on the page. If you don't care about these errors, you can ignore them by setting js_errors: false in your Poltergeist configuration (see documentation for details).
Possibly unhandled rejection: {"data":"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0//EN\">\n<HTML>\n <HEAD><TITLE>Internal Server Error</TITLE></HEAD>\n <BODY>\n <H1>Internal Server Error</H1>\n undefined method `split' for 1:Fixnum\n <HR>\n <ADDRESS>\n WEBrick/1.3.1 (Ruby/2.3.3/2016-11-21) at\n 127.0.0.1:54674\n </ADDRESS>\n </BODY>\n</HTML>\n","status":500,"config":{"method":"GET","transformRequest":[null],"transformResponse":[null],"jsonpCallbackParam":"callback","url":"/tickets","params":{},"headers":{"Accept":"application/json","Content-Type":"application/json"},"data":{}},"statusText":"Internal Server Error "}
Possibly unhandled rejection: {"data":"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0//EN\">\n<HTML>\n <HEAD><TITLE>Internal Server Error</TITLE></HEAD>\n <BODY>\n <H1>Internal Server Error</H1>\n undefined method `split' for 1:Fixnum\n <HR>\n <ADDRESS>\n WEBrick/1.3.1 (Ruby/2.3.3/2016-11-21) at\n 127.0.0.1:54674\n </ADDRESS>\n </BODY>\n</HTML>\n","status":500,"config":{"method":"GET","transformRequest":[null],"transformResponse":[null],"jsonpCallbackParam":"callback","url":"/tickets","params":{},"headers":{"Accept":"application/json","Content-Type":"application/json"},"data":{}},"statusText":"Internal Server Error "}
at http://127.0.0.1:54674/assets/application-713835b1641be632b29f7502c00a879e171bca5d6d06a5f264afcd819c123e76.js:14363
Here is my stack:
rails (5.0.2)
capybara (2.12.1)
poltergeist (1.13.0)
rspec-core (3.5.4)
phantomjs 2.1.1
Additional info:
If I output something right before ending the controller action, it gets outputted. The execution is going through the entire action;
If I console.log something right before fetching tickets, it's outputted as well. However, the Promise is not being resolved.
I found out the issue was with my pagination headers (x-total-count and x-per-page). Converting them to String does the trick. The weird part is that it was working OK in development, but not in test environment. So, if anyone has this issue in the future, the solution in my case was:
def index
#tickets = apply_scopes(#tickets)
response.headers['x-total-count'] = #tickets.total_count.to_s
response.headers['x-per-page'] = Ticket.default_per_page.to_s
end
Notice .to_s being called when assigning the headers.

RSpec before suite not being run

I'm trying to stub any external API calls in my test suite, but the before(:suite) is never executed. Webmock always reports that I need to stub the maps.googleapis.com even though no tests have been run yet (no green dots, no red Fs).
spec_helper.rb:
require 'webmock/rspec'
WebMock.disable_net_connect!(allow_localhost: true)
...
config.before(:suite) do
puts "THIS NEVER SHOWS"
stub_request(:get, "maps.googleapis.com").
with(headers: {'Accept'=>'*/*', 'User-Agent'=>'Ruby'}).
to_return(status: 200, body: "", headers: {})
end
The geocoder gem ends up trying to save the lat/lon from googleapis.com and an error is raised by Webmock saying that the URL is unregistered.
EDIT: Error snippet:
$ bundle exec rspec spec/factories_spec.rb
/home/jake/.rvm/gems/ruby-2.1.0#global/gems/webmock-1.17.4/lib/webmock/http_lib_adapters/net_http.rb:114:in `request': Real HTTP connections are disabled. Unregistered request: GET http://maps.googleapis.com/maps/api/geocode/json?address=[private]&language=en&sensor=false with headers {'Accept'=>'*/*', 'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'User-Agent'=>'Ruby'} (WebMock::NetConnectNotAllowedError)
You can stub this request with the following snippet:
stub_request(:get, "http://maps.googleapis.com/maps/api/geocode/json?address=[private]&language=en&sensor=false").
with(:headers => {'Accept'=>'*/*', 'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'User-Agent'=>'Ruby'}).
to_return(:status => 200, :body => "", :headers => {})
============================================================
from /home/jake/.rvm/gems/ruby-2.1.0#global/gems/geocoder-1.1.9...
...
Again, I'll stress that this has to do with the fact that the code in the config.before(:each) block is never run. Why? Because if it was, I could "raise 'WTF'" and 'WTF' should appear in the console output instead of the error you see above. I only see 'WTF' when I "un-bundle" the Webmock gem.
Well I was doing "something cute" with my RSpec tests by creating tests at runtime depending on whether or not the Factory has an attribute that is a file. Due to the way my factories/models were set up, factories were being created (saved) when the attributes for a certain factory were being read, so the block of code that's generating the tests runs outside of RSpec's config.before(:suite) and WebMock raises the error.
https://github.com/bblimke/webmock/issues/378
Moreover, here's specifically what I was doing wrong - not related to WebMock:
1) In my factories.rb, I was calling create() for associations which may not yet exist. Why? Because RSpec was giving me errors saying "[association] was blank". It was doing that because I had validates_presence_of :association_id instead of just :association. When I used create() instead of build(), it "worked". Of course when it came time to use WebMock, I was creating (and thus saving) objects calling geocoder to do it's thing. The solution was to fix validates_presence_of to use the right attribute and use build() instead of create() in my factories.
Bad Example:
# In spec/factories.rb
factory :review, class: Manager::Review do
rating 4
wine { Manager::Wine.first || create(:wine) }
reviewer { Manager::Reviewer.first || create(:reviewer) }
date Time.now
association :referral, referrable_id: 1, referrable_type: Manager::Review, strategy: :build
end
# In app/models/manager/review.rb
validates_presence_of :rating_id, :wine_id, :reviewer_id, :date
Good Example:
# In spec/factories.rb
factory :review, class: Manager::Review do
rating 4
wine { Manager::Wine.first || build(:wine) }
reviewer { Manager::Reviewer.first || build(:reviewer) }
date Time.now
association :referral, referrable_id: 1, referrable_type: Manager::Review, strategy: :build
end
# In app/models/manager/review.rb
validates_presence_of :rating, :wine, :reviewer, :date
2) FWIW, I told geocoder to fetch the geocode before_save, not after_validate like it suggests in their home page.
Also, you cannot stub with WebMock in the before(:suite), but it works in before(:each)

ActionMailer Observer not working

I defined an ActionMailer Observer and it's not being called. How can I go about troubleshooting this? Rails 3.1.3, Ruby 1.9.2. Alternatively, I want a method called whenever an email is sent form a certain ActionMailer subclass. This is the only clean way I could find to implement this (I could call a method from each mailing method in the ActionMailer subclass, but that's not very DRY). Any other suggestions as to how to accomplish this would be welcome.
config/initializers/mail_observer.rb:
class MailObserver
def self.delivered_email(message)
require 'ruby-debug' ; debugger
user = User.find_by_email(message.to[0])
if user
email_type = caller[1]
UserMailerLogging.create!(user_id: #user.id, email_type: email_type, contents: self.body)
end
end
end
ActionMailer::Base.register_observer(MailObserver)
app/mailers/user_mailer:
class UserMailer < ActionMailer::Base
default from: "no-reply#testing.com"
...
def unfinished_setup(user)
#user = user
mail to: user_email(user), subject: "Testing..."
end
...
end
And, in the Rails Console:
irb(main):001:0> UserMailer.unfinished_setup(User.find(1))
=> #<Mail::Message:70192154395900, Multipart: false, Headers: <From: no-reply#testing.com>, <To: Test User<user#testing.com>>, <Subject: Testing...>, <Mime-Version: 1.0>, <Content-Type: text/plain>>
I believe the observer won't fire until you actually deliver the message. Your code in the rails console is only creating the email, not sending it. Adding .deliver to the end should deliver the message and execute the observer code.

Resources