RSpec Rails mailer expectation - last delivery is of specific action/method/template - ruby-on-rails
I have the following task:
My mailer has multiple actions/templates, each with the same static hardcoded subject.
In my RSpec expectation I would like to verify that certain block triggers email(changes ActionMailer::Base.deliveries.last&.something) of specific mailer action/template.
Limitations:
can not rely on a delivery subject/title(not unique)
preferably avoid checking delivery content(they are mostly the same except for a few minor wording changes)
can not change a delivery subject/title generation just to make this spec pass
can not mock/stub mailer(I don't want to skip the rendering part, this is also a part of the test)
Methods that I tried that didn't work:
ActionMailer::Base.deliveries.last.template_model
# => nil
ActionMailer::Base.deliveries.last.template_alias
# => nil
ActionMailer::Base.deliveries.last.filename
# => nil
ActionMailer::Base.deliveries.last.metadata
# => {}
ActionMailer::Base.deliveries.last.action
# => nil
What might be an idiomatic Rails way of approaching this task? Custom header with action/template name?
Mail::Message methods(for the reference):
ActionMailer::Base.deliveries.last.public_methods(false)
=> [:perform_deliveries=,
:mime_version=,
:sender,
:metadata=,
:boundary,
:multipart?,
:errors,
:to_yaml,
:delivery_handler,
:tag,
:raise_delivery_errors,
:register_for_delivery_notification,
:inform_observers,
:inform_interceptors,
:in_reply_to=,
:comments=,
:in_reply_to,
:reply_to,
:references=,
:message_id=,
:raw_source,
:set_envelope,
:filename,
:raw_envelope,
:envelope_from,
:envelope_date,
:content_description,
:content_description=,
:content_disposition=,
:content_id=,
:content_location,
:content_location=,
:content_transfer_encoding,
:content_transfer_encoding=,
:transport_encoding,
:transport_encoding=,
:received,
:received=,
:header=,
:reply_to=,
:resent_bcc,
:resent_bcc=,
:resent_cc,
:resent_cc=,
:resent_date,
:resent_date=,
:resent_from,
:resent_from=,
:bcc,
:resent_message_id=,
:resent_message_id,
:resent_sender=,
:resent_sender,
:resent_to=,
:resent_to,
:references,
:from,
:return_path=,
:sender=,
:return_path,
:smtp_envelope_from=,
:smtp_envelope_to=,
:smtp_envelope_from,
:body_encoding,
:body_encoding=,
:smtp_envelope_to,
:cc_addrs,
:bcc_addrs,
:from_addrs,
:has_message_id?,
:has_date?,
:header_fields,
:has_charset?,
:has_content_transfer_encoding?,
:has_mime_version?,
:has_transfer_encoding?,
:destinations,
:add_date,
:add_message_id,
:add_mime_version,
:sub_type,
:to,
:subject,
:subject=,
:cc=,
:bcc=,
:add_content_transfer_encoding,
:perform_deliveries,
:<=>,
:message_content_type,
:delivery_status_part,
:add_content_type,
:add_charset,
:==,
:main_type,
:transfer_encoding,
:diagnostic_code,
:mime_parameters,
:[],
:[]=,
:remote_mta,
:retryable?,
:html_part=,
:bounced?,
:final_recipient,
:add_transfer_encoding,
:text_part=,
:convert_to_multipart,
:attachment,
:keywords,
:charset=,
:delivery_handler=,
:without_attachments!,
:error_status,
:delivery_status_report?,
:decode_body,
:all_parts,
:skip_deletion,
:mark_for_delete=,
:is_marked_for_delete?,
:inspect,
:multipart_report?,
:method_missing,
:attachment?,
:mime_version,
:add_file,
:action,
:raise_delivery_errors=,
:parts,
:to_s,
:body=,
:content_type_parameters,
:has_attachments?,
:add_part,
:find_first_mime_type,
:encoded,
:date,
:decoded,
:attachments,
:track_links,
:part,
:templated?,
:metadata,
:export_headers,
:track_opens,
:template_alias,
:template_model,
:message_stream,
:export_attachments,
:body_html,
:body_text,
:message_id,
:comments,
:to_addrs,
:from=,
:postmark_response,
:default,
:to_postmark_hash,
:content_disposition,
:content_id,
:has_content_type?,
:keywords=,
:delivery_method,
:date=,
:text?,
:cc,
:delivered=,
:postmark_response=,
:encode!,
:mime_type,
:html?,
:tag=,
:delivered,
:delivered?,
:track_links=,
:track_opens=,
:postmark_attachments=,
:postmark_attachments,
:read,
:message_stream=,
:prerender,
:text_part,
:html_part,
:reply,
:ready_to_send!,
:template_model=,
:deliver!,
:headers,
:header,
:content_type,
:to=,
:charset,
:deliver,
:body,
:content_type=]
Related question:
Rails, RSpec: How to test, that a specific mailer action gets triggered (uses mocks)
That's what I currently ended up with(until I find a better way):
added the following line to the each of related mailer actions:
headers['X-Rails-Template'] = __method__
add my specs looks a bit like this:
subject { -> { described_class.call } }
let(:last_template_name) do
-> do
m = ActionMailer::Base.deliveries.last
next if m.blank?
m.header['X-Rails-Template']&.value
end
end
it do
is_expected.to change { ActionMailer::Base.deliveries.count }.to(1)
.and change(&last_template_name).to('mailer_action_name1')
end
Related
activeadmin and dynamic store accessors fails on new resource
I want to generate forms for a resource that has a postgres jsonb column :data, and I want the schema for these forms to be stored in a table in the database. After a lot of research I am 90% there but my method fails in ActiveAdmin forms upon create (not update). Can anyone explain this? Sorry for the long code snippets. This is a fairly elaborate setup but I think it would be of some interest since if this works one could build arbitrary new schemas dynamically without hard-coding. I am following along this previous discussion with Rails 6 and ActiveAdmin 2.6.1 and ruby 2.6.5. I want to store Json Schemas in a table SampleActionSchema that belong_to SampleAction (using the json-schema gem for validation) class SampleActionSchema < ApplicationRecord validates :category, uniqueness: { case_sensitive: false }, allow_nil: false, allow_blank: true validate :schema_is_json_schema private def schema_is_json_schema metaschema = JSON::Validator.validator_for_name("draft4").metaschema unless JSON::Validator.validate(metaschema, schema) errors.add :schema, 'not a compliant json schema' end end end class SampleAction < ActiveRecord::Base belongs_to :sample validate :is_sample_action validates :name, uniqueness: { case_sensitive: false } after_initialize :add_field_accessors before_create :add_field_accessors before_update :add_field_accessors def add_store_accessor field_name singleton_class.class_eval {store_accessor :data, field_name.to_sym} end def add_field_accessors num_fields = schema_properties.try(:keys).try(:count) || 0 schema_properties.keys.each {|field_name| add_store_accessor field_name} if num_fields > 0 end def schema_properties schema_arr=SampleActionSchema.where(category: category) if schema_arr.size>0 sc=schema_arr[0] if !sc.schema.empty? props=sc.schema["properties"] else props=[] end else [] end end private def is_sample_action sa=SampleActionSchema.where(category: category) errors.add :category, 'not a known sample action' unless (sa.size>0) errors.add :base, 'incorrect json format' unless (sa.size>0) && JSON::Validator.validate(sa[0].schema, data) end end This all works correctly; For example, for a simple schema called category: "cleave", where :data looks like data: {quality: "good"}, I can create a resource as follows in the rails console: sa=SampleAction.new(sample_id: 6, name: "test0", data: {}, category: "cleave" ) => #<SampleAction id: nil, name: "test0", category: "cleave", data: {}, created_at: nil, updated_at: nil, sample_id: 6> sa.quality = "good" => true sa.save => true To make this system work in AA forms, I call the normal path (new or edit)_admix_sample_action_form with params: {category: "cleave"} and then I generate permit_params dynamically: ActiveAdmin.register SampleAction, namespace: :admix do permit_params do prms=[:name, :category, :data, :sample_id, :created_at, :updated_at] #the first case is creating a new record (gets parameter from admix/sample_actions/new?category="xxx" #the second case is updating an existing record #falls back to blank (no extra parameters) categ = #_params[:category] || (#_params[:sample_action][:category] if #_params[:sample_action]) || nil cat=SampleActionSchema.where(category: categ) if cat.size>0 && !cat[0].schema.empty? cat[0].schema["properties"].each do |key, value| prms+=[key.to_sym] end end prms end form do |f| f.semantic_errors new=f.object.new_record? cat=params[:category] || f.object.category f.object.category=cat if cat && new f.object.add_field_accessors if new sas=SampleActionSchema.where(category: cat) is_schema=(sas.size>0) && !sas[0].schema.empty? if session[:active_sample] f.object.sample_id=session[:active_sample] end f.inputs "Sample Action" do f.input :sample_id f.input :name f.input :category if !is_schema f.input :data, as: :jsonb else f.object.schema_properties.each do |key, value| f.input key.to_sym, as: :string end end end f.actions end Everything works fine if I am editing an existing resource (as created in the console above). The form is displayed and all the dynamic fields are updated upon submit. But when creating a new resource where e.g. :data is of the form data: {quality: "good"} I get ActiveModel::UnknownAttributeError in Admix::SampleActionsController#create unknown attribute 'quality' for SampleAction. I have tried to both add_accessors in the form and to override the new command to add the accessors after initialize (these should not be needed because the ActiveRecord callback appears to do the job at the right time). def new build_resource resource.add_field_accessors new! end Somehow when the resource is created in the AA controller, it seems impossible to get the accessors stored even though it works fine in the console. Does anyone have a strategy to initialize the resource correctly?
SOLUTION: I traced what AA was doing to figure out the minimum number of commands needed. It was necessary to add code to build_new_resource to ensure that any new resource AA built had the correct :category field, and once doing so, make the call to dynamically add the store_accessor keys to the newly built instance. Now users can create their own original schemas and records that use them, without any further programming! I hope others find this useful, I certainly will. There are a couple ugly solutions here, one is that adding the parameters to the active admin new route call is not expected by AA, but it still works. I guess this parameter could be passed in some other way, but quick and dirty does the job. The other is that I had to have the form generate a session variable to store what kind of schema was used, in order for the post-form-submission build to know, since pressing the "Create Move" button clears the params from the url. The operations are as follows: for a model called Move with field :data that should be dynamically serialized into fields according to the json schema tables, both admin/moves/new?category="cleave" and admin/moves/#/edit find the "cleave" schema from the schema table, and correctly create and populate a form with the serialized parameters. And, direct writes to the db m=Move.new(category: "cleave") ==> true m.update(name: "t2", quality: "fine") ==> true work as expected. The schema table is defined as: require "json-schema" class SampleActionSchema < ApplicationRecord validates :category, uniqueness: { case_sensitive: false }, allow_nil: false, allow_blank: true validate :schema_is_json_schema def self.schema_keys(categ) sas=SampleActionSchema.find_by(category: categ) schema_keys= sas.nil? ? [] : sas[:schema]["properties"].keys.map{|k| k.to_sym} end private def schema_is_json_schema metaschema = JSON::Validator.validator_for_name("draft4").metaschema unless JSON::Validator.validate(metaschema, schema) errors.add :schema, 'not a compliant json schema' end end end The Move table that employs this schema is: class Move < ApplicationRecord after_initialize :add_field_accessors def add_field_accessors if category!="" keys=SampleActionSchema.schema_keys(category) keys.each {|k| singleton_class.class_eval{store_accessor :data, k}} end end end Finally, the working controller: ActiveAdmin.register Move do permit_params do #choice 1 is for new records, choice 2 is for editing existing categ = #_params[:category] || (#_params[:move][:category] if #_params[:move]) || "" keys=SampleActionSchema.schema_keys(categ) prms = [:name, :data] + keys end form do |f| new=f.object.new_record? f.object.category=params[:category] if new if new session[:current_category]=params[:category] f.object.add_field_accessors else session[:current_category] = "" end keys=SampleActionSchema.schema_keys(f.object.category) f.inputs do f.input :name f.input :category keys.each {|k| f.input k} end f.actions end controller do def build_new_resource r=super r.assign_attributes(category: session[:current_category]) r.add_field_accessors r end end end
Ruby On Rails delayed_job not calling method
Does anyone know why the following will not work. Is it my code? I'm trying to call the assign_picks at specific times but I could not get it to work. I then tried the below (in 5 minutes time). I've been following the instructions in this stackoverflow post I rolled back the db and then migrated again but nothing happens after 5 minutes. I've tested the assign_picks method by calling it on a button press and it works fine. Any ideas? I'v been stuck here for hours. Thanks for reading. Edit: I meant to mention that I'm using a virtual machine on Nitrious.io. Would that have anything to do with it? Models/match.rb class Match < ActiveRecord::Base attr_accessible :block, :round, :date, :day, :time, :venue, :team1, :team2, :played, :result, :resultString has_many :match_picks has_many :current_blocks after_create :set_expiry_timer # register the timer def set_expiry_timer delay(:run_at => 5.minutes.from_now).assign_picks end def assign_picks #current_blocks = CurrentBlock.where(:id => 1) #users = User.where(:curr_block != #current_blocks[0].block) #matches = Match.where(:block => #current_blocks[0].block) #users.each do |user| #matches.each do |match| MatchPick.create!(:userID => user.id, :matchID => match.id, :blockID => #current_blocks[0].block, :userPick => (0..2).to_a.sample) end end # Increase the current block for application CurrentBlock.where(:id => 1).each do |cb| cb.update_attribute(:block, cb.block + 1) end end end
Rails - Custom Validation For Having A Single Value Once
so I have these two models: class Tag < ActiveRecord::Base has_many :event_tags attr_accessible :tag_id, :tag_type, :value end class EventTag < ActiveRecord::Base belongs_to :tag attr_accessible :tag_id, :event_id, :region end and this table for Tags: **tag_id** **tag_type** **value** 1 "funLevel" "Boring..." 2 "funLevel" "A Little" 3 "funLevel" "Hellz ya" 4 "generic" "Needs less clowns" 5 "generic" "Lazer Tag" ... What I would like to do is write a custom validation where it checks to see: Each event_id has only one tag_type of "funLevel" attached to it, but can have more than one "generic" tags For example: t1 = EventTag.new(:tag_id => 1, :event_id =>777, :region => 'US') t1.save # success t2 = EventTag.new(:tag_id => 2, :event_id =>777, :region => 'US') t2.save # failure # because (event_id: 777) already has a tag_type of # "funLevel" associated with it t3 = EventTag.new(:tag_id => 4, :event_id =>777, :region => 'US') t3.save # success, because as (tag_id:4) is not "funLevel" type I have come up with one ugly solution: def cannot_have_multiple_funLevel_tag list_of_tag_ids = EventTag.where("event_id = ?", event_id).pluck(:tag_id) if(Tag.where("tag_id in ?", list_of_tag_ids).pluck(:tag_type).include? "funLevel") errors.add(:tag_id, "Already has a Fun Level Tag!") end Being new to rails, is there a more better/more elegant/more inexpensive way?
The way you have your data structured means that the inbuilt Rails validations are probably not going to be a heap of help to you. If the funLevel attribute was directly accessible by the EventTag class, you could just use something like: # event_tag.rb validate :tag_type, uniqueness: { scope: :event_id }, if: Proc.new { |tag| tag.tag_type == "funLevel" } (unfortunately, from a quick test you don't seem to be able to validate the uniqueness of a virtual attribute.) Without that, you're probably stuck using a custom validation. The obvious improvement to the custom validation you have (given it looks like you want to have the validation on EventTag) would be to not run the validation unless that EventTag is a funLevel tag: def cannot_have_multiple_funLevel_tag return unless self.tag.tag_type == "funLevel" ... end
Testing named_scope with Mongoid and rspec
I'm newbie in RoR, and I am trying to test a simple named_scope for my Model. But I don't know if I have a problem in my model (I'm using mongoid), in my code test (I'm using rspec) or in my factory. I got this error Mongoid::Errors::InvalidCollection: Access to the collection for Movement is not allowed since it is an embedded document, please access a collection from the root document. My models class Movement include Mongoid::Document field :description, :type => String embedded_in :category named_scope :top, lambda { |number| { :limit => (number.size > 0 ? number : 10) } } end class Category include Mongoid::Document field :name embeds_many :movement end My factory, con factory_girl Factory.define :movement do |m| m.amount 24 m.date "30/10/2011" m.description "Beer" m.association :category, :factory => :category end Factory.define :category do |c| c.name "Drink" end My test describe "when i have a movement list" do it "recent method should return last 2 movements" do #movements = (1..3).collect { Factory(:movement) } recent_movements = Movement.top(2) recent_movements.should have(2).entries end end And the error: Mongoid::Errors::InvalidCollection: Access to the collection for Movement is not allowed since it is an embedded >document, please access a collection from the root document. I tried a little change in my factory. Factory.define :movement do |m| m.amount 24 m.date "30/10/2011" m.description "Beer" m.category { [ Factory.build(:category) ] } end But then I got other different error: Failure/Error: #movements = (1..3).collect { Factory(:movement) } NoMethodError: undefined method `reflect_on_association' for # Could someone help me? Thanks
I just had a the same error in my app. I ended up having an error in my class and that solved my problem.
save! method for referenced attributes in mongoid
I use Rails 3.0.6 with mongoID 2.0.2. Recently I encountered an issue with save! method when overriding setter (I am trying to create my own nested attributes). So here is the model: class FeedItem include Mongoid::Document has_many :audio_refs def audio_refs=(attributes_array, binding) attributes_array.each do |attributes| if attributes[:audio_track][:id] self.audio_refs.build(:audio_track => AudioTrack.find(attributes[:audio_track][:id])) elsif attributes[:audio_track][:file] self.audio_refs.build(:audio_track => AudioTrack.new(:user_id => attributes[:audio_track][:user_id], :file => attributes[:audio_track][:file])) end end if !binding self.save! end end AudioRef model (which is just buffer between audio_tracks and feed_items) is: class AudioRef include Mongoid::Document belongs_to :feed_item belongs_to :audio_track end And AudioTrack: class AudioTrack include Mongoid::Document has_many :audio_refs mount_uploader :file, AudioUploader end So here is the spec for the FeedItem model which doesn`t work: it "Should create audio_track and add audio_ref" do #audio_track = Fabricate(:audio_track, :user_id => #author.id, :file => File.open("#{Rails.root}/spec/stuff/test.mp3")) #feed_item= FeedItem.new( :user => #author, :message => {:body => Faker::Lorem.sentence(4)}, :audio_refs => [ {:audio_track => {:id => #audio_track.id}}, {:audio_track => {:user_id => #author.id, :file => File.open("#{Rails.root}/spec/stuff/test.mp3")}} ] ) #feed_item.save! #feed_item.reload #feed_item.audio_refs.length.should be(2) end As you can see, the reason I am overriding audio_refs= method is that FeedItem can be created from existing AudioTracks (when there is params[:audio_track][:id]) or from uploaded file (params[:audio_track][:file]). The problem is that #feed_item.audio_refs.length == 0 when I run this spec, i.e. audio_refs are not saved. Could you please help me with that? Some investigation: 1) binding param is "true" by default (this means we are in building mode)
I found a solution to my problem but I didnt understand why save method doesnt work and didn`t make my code work. So first of all let me describe my investigations about the problem. After audio_refs= is called an array of audio_refs is created BUT in any audio_ref is no feed_item_id. Probably it is because the feed_item is not saved by the moment. So the solution is quite simple - Virtual Attributes. To understand them watch corresponding railscasts So my solution is to create audio_refs by means of callback "after_save" I slightly changed my models: In FeedItem.rb I added attr_writer :audio_tracks #feed_item operates with audio_tracks array after_save :assign_audio #method to be called on callback def assign_audio if #audio_tracks #audio_tracks.each do |attributes| if attributes[:id] self.audio_refs << AudioRef.new(:audio_track => AudioTrack.find(attributes[:id])) elsif attributes[:file] self.audio_refs << AudioRef.new(:audio_track => AudioTrack.new(:user_id => attributes[:user_id], :file => attributes[:file])) end end end end And the spec is now: it "Should create audio_track and add audio_ref" do #audio_track = Fabricate(:audio_track, :user_id => #author.id, :file => File.open("#{Rails.root}/spec/stuff/test.mp3")) #feed_item= FeedItem.new( :user => #author, :message => {:body => Faker::Lorem.sentence(4)}, :audio_tracks => [ {:id => #audio_track.id}, {:user_id => #author.id, :file => File.open("#{Rails.root}/spec/stuff/test.mp3")} ] ) #feed_item.save! #feed_item.reload #feed_item.audio_refs.length.should be(2) end And it works fine!!! Good luck with your coding)
Check that audio_refs=() is actually being called, by adding debug output of some kind. My feeling is that your FeedItem.new() call doesn't use the audio_refs=() setter. Here's the source code of the ActiveRecord::Base#initialize method, taken from APIdock: # File activerecord/lib/active_record/base.rb, line 1396 def initialize(attributes = nil) #attributes = attributes_from_column_definition #attributes_cache = {} #new_record = true #readonly = false #destroyed = false #marked_for_destruction = false #previously_changed = {} #changed_attributes = {} ensure_proper_type populate_with_current_scope_attributes self.attributes = attributes unless attributes.nil? result = yield self if block_given? _run_initialize_callbacks result end I don't currently have an environment to test this, but it looks like it's setting the attributes hash directly without going through each attribute's setter. If that's the case, you'll need to call your setter manually. Actually, I think the fact you're not getting an exception for the number of arguments (binding not set) proves that your setter isn't being called.