diff options
Diffstat (limited to 'spec/models')
26 files changed, 2074 insertions, 1041 deletions
diff --git a/spec/models/application_mailer_spec.rb b/spec/models/application_mailer_spec.rb deleted file mode 100644 index acf5f43bc..000000000 --- a/spec/models/application_mailer_spec.rb +++ /dev/null @@ -1,162 +0,0 @@ -require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') - - -describe ApplicationMailer do - - context 'when using plugins' do - - def set_base_views - ApplicationMailer.class_eval do - @previous_view_paths = self.view_paths.dup - self.view_paths.clear - self.view_paths << File.join(Rails.root, 'spec', 'fixtures', 'theme_views', 'core') - end - end - - def add_mail_methods(method_names) - method_names.each{ |method_name| ApplicationMailer.send(:define_method, method_name){} } - end - - def remove_mail_methods(method_names) - method_names.each do |method_name| - if ApplicationMailer.respond_to?(method_name) - ApplicationMailer.send(:remove_method, method_name) - end - end - end - - def prepend_theme_views(theme_name) - ApplicationMailer.class_eval do - view_paths.unshift File.join(Rails.root, 'spec', 'fixtures', 'theme_views', theme_name) - end - end - - def append_theme_views(theme_name) - ApplicationMailer.class_eval do - view_paths << File.join(Rails.root, 'spec', 'fixtures', 'theme_views', theme_name) - end - end - - def reset_views - ApplicationMailer.class_eval do - self.view_paths = @previous_view_paths - end - end - - def create_multipart_method(method_name) - ApplicationMailer.send(:define_method, method_name) do - attachment :content_type => 'message/rfc822', - :body => 'xxx', - :filename => "original.eml", - :transfer_encoding => '7bit', - :content_disposition => 'inline' - end - end - - before do - set_base_views - add_mail_methods(['simple', 'theme_only', 'core_only', 'neither']) - end - - describe 'when a plugin prepends its mail templates to the view paths' do - - it 'should render a theme template in preference to a core template' do - prepend_theme_views('theme_one') - @mail = ApplicationMailer.create_simple() - @mail.body.should match('Theme simple') - end - - it 'should render the template provided by the theme if no template is available in core' do - prepend_theme_views('theme_one') - @mail = ApplicationMailer.create_theme_only() - @mail.body.should match('Theme only') - end - - it 'should render the template provided by core if there is no theme template' do - prepend_theme_views('theme_one') - @mail = ApplicationMailer.create_core_only() - @mail.body.should match('Core only') - end - - it 'should raise an error if the template is in neither core nor theme' do - prepend_theme_views('theme_one') - expected_error = 'Missing template application_mailer/neither.erb in view path' - lambda{ ApplicationMailer.create_neither() }.should raise_error(/#{expected_error}/) - end - - it 'should render a multipart email using a theme template' do - prepend_theme_views('theme_one') - create_multipart_method('multipart_theme_only') - @mail = ApplicationMailer.create_multipart_theme_only() - @mail.parts.size.should == 2 - message_part = @mail.parts[0].to_s - message_part.should match("Theme multipart") - end - - it 'should render a multipart email using a core template' do - prepend_theme_views('theme_one') - create_multipart_method('multipart_core_only') - @mail = ApplicationMailer.create_multipart_core_only() - @mail.parts.size.should == 2 - message_part = @mail.parts[0].to_s - message_part.should match("Core multipart") - end - - end - - describe 'when a plugin appends its mail templates to the view paths' do - - it 'should render a core template in preference to a theme template' do - append_theme_views('theme_one') - @mail = ApplicationMailer.create_simple() - @mail.body.should match('Core simple') - end - - it 'should render the template provided by the theme if no template is available in core' do - append_theme_views('theme_one') - @mail = ApplicationMailer.create_theme_only() - @mail.body.should match('Theme only') - end - - it 'should render the template provided by core if there is no theme template' do - append_theme_views('theme_one') - @mail = ApplicationMailer.create_core_only() - @mail.body.should match('Core only') - end - - it 'should raise an error if the template is in neither core nor theme' do - append_theme_views('theme_one') - expected_error = 'Missing template application_mailer/neither.erb in view path' - lambda{ ApplicationMailer.create_neither() }.should raise_error(/#{expected_error}/) - end - - it 'should render a multipart email using a core template' do - append_theme_views('theme_one') - create_multipart_method('multipart_core_only') - @mail = ApplicationMailer.create_multipart_core_only() - @mail.parts.size.should == 2 - message_part = @mail.parts[0].to_s - message_part.should match("Core multipart") - end - - it 'should render a multipart email using a theme template' do - append_theme_views('theme_one') - create_multipart_method('multipart_theme_only') - @mail = ApplicationMailer.create_multipart_theme_only() - @mail.parts.size.should == 2 - message_part = @mail.parts[0].to_s - message_part.should match("Theme multipart") - end - - end - - after do - reset_views - remove_mail_methods(['simple', 'theme_only', 'core_only', 'neither', 'multipart']) - end - end - -end - - - diff --git a/spec/models/censor_rule_spec.rb b/spec/models/censor_rule_spec.rb index c11b05a03..5b41cc0d4 100644 --- a/spec/models/censor_rule_spec.rb +++ b/spec/models/censor_rule_spec.rb @@ -1,3 +1,20 @@ +# == Schema Information +# +# Table name: censor_rules +# +# id :integer not null, primary key +# info_request_id :integer +# user_id :integer +# public_body_id :integer +# text :text not null +# replacement :text not null +# last_edit_editor :string(255) not null +# last_edit_comment :text not null +# created_at :datetime not null +# updated_at :datetime not null +# regexp :boolean +# + require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') describe CensorRule, "substituting things" do @@ -73,10 +90,10 @@ end describe 'when validating rules' do - describe 'should be invalid without text' do + it 'should be invalid without text' do censor_rule = CensorRule.new censor_rule.valid?.should == false - censor_rule.errors.on(:text).should == "can't be blank" + censor_rule.errors[:text].should == ["can't be blank"] end describe 'when validating a regexp rule' do @@ -96,7 +113,7 @@ describe 'when validating rules' do it 'should add an error message to the text field with the regexp error message' do Regexp.stub!(:new).and_raise(RegexpError.new("very bad regexp")) @censor_rule.valid?.should == false - @censor_rule.errors.on(:text).should == "very bad regexp" + @censor_rule.errors[:text].should == ["very bad regexp"] end end @@ -106,7 +123,7 @@ describe 'when validating rules' do it 'should not add any error message to the text field' do Regexp.stub!(:new) @censor_rule.valid? - @censor_rule.errors.on(:text).should == nil + @censor_rule.errors[:text].should == [] end end @@ -134,15 +151,21 @@ describe 'when validating rules' do it 'should not allow a global text censor rule (without user_id, request_id or public_body_id)' do @censor_rule.valid?.should == false - @expected_error = 'Censor must apply to an info request a user or a body; is invalid' - @censor_rule.errors.full_messages.should == [@expected_error] + + expected_error = ["Rule must apply to an info request, a user or a body"] + @censor_rule.errors[:user].should == expected_error + @censor_rule.errors[:info_request].should == expected_error + @censor_rule.errors[:public_body].should == expected_error end it 'should not allow a global regex censor rule (without user_id, request_id or public_body_id)' do @censor_rule.regexp = true @censor_rule.valid?.should == false - @expected_error = 'Censor must apply to an info request a user or a body; is invalid' - @censor_rule.errors.full_messages.should == [@expected_error] + + expected_error = ["Rule must apply to an info request, a user or a body"] + @censor_rule.errors[:user].should == expected_error + @censor_rule.errors[:info_request].should == expected_error + @censor_rule.errors[:public_body].should == expected_error end end diff --git a/spec/models/contact_mailer_spec.rb b/spec/models/contact_mailer_spec.rb deleted file mode 100644 index 202e45758..000000000 --- a/spec/models/contact_mailer_spec.rb +++ /dev/null @@ -1,8 +0,0 @@ -require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') - -describe ContactMailer, " when blah" do - before do - end -end - - diff --git a/spec/models/foi_attachment_spec.rb b/spec/models/foi_attachment_spec.rb index 537a3363c..882723d1e 100644 --- a/spec/models/foi_attachment_spec.rb +++ b/spec/models/foi_attachment_spec.rb @@ -1,6 +1,21 @@ +# == Schema Information +# +# Table name: foi_attachments +# +# id :integer not null, primary key +# content_type :text +# filename :text +# charset :text +# display_size :text +# url_part_number :integer +# within_rfc822_subject :text +# incoming_message_id :integer +# hexdigest :string(32) +# + require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') -describe FoiAttachment, " when calculating due date" do +describe FoiAttachment do before(:each) do load_raw_emails_data diff --git a/spec/models/holiday_spec.rb b/spec/models/holiday_spec.rb index 5d3f76d24..89849abb7 100644 --- a/spec/models/holiday_spec.rb +++ b/spec/models/holiday_spec.rb @@ -1,3 +1,12 @@ +# == Schema Information +# +# Table name: holidays +# +# id :integer not null, primary key +# day :date +# description :text +# + require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') describe Holiday, " when calculating due date" do diff --git a/spec/models/incoming_message_spec.rb b/spec/models/incoming_message_spec.rb index f53a5856a..f06dcbeeb 100644 --- a/spec/models/incoming_message_spec.rb +++ b/spec/models/incoming_message_spec.rb @@ -1,6 +1,140 @@ # coding: utf-8 +# == Schema Information +# +# Table name: incoming_messages +# +# id :integer not null, primary key +# info_request_id :integer not null +# created_at :datetime not null +# updated_at :datetime not null +# raw_email_id :integer not null +# cached_attachment_text_clipped :text +# cached_main_body_text_folded :text +# cached_main_body_text_unfolded :text +# subject :text +# mail_from_domain :text +# valid_to_reply_to :boolean +# last_parsed :datetime +# mail_from :text +# sent_at :datetime +# prominence :string(255) default("normal"), not null +# prominence_reason :text +# + require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') +describe IncomingMessage, 'when validating' do + + it 'should be valid with valid prominence values' do + ['hidden', 'requester_only', 'normal'].each do |prominence| + incoming_message = IncomingMessage.new(:raw_email => RawEmail.new, + :info_request => InfoRequest.new, + :prominence => prominence) + incoming_message.valid?.should be_true + end + end + + it 'should not be valid with an invalid prominence value' do + incoming_message = IncomingMessage.new(:raw_email => RawEmail.new, + :info_request => InfoRequest.new, + :prominence => 'norman') + incoming_message.valid?.should be_false + end + +end + +describe IncomingMessage, 'when getting a response event' do + + it 'should return an event with event_type "response"' do + incoming_message = IncomingMessage.new + ['comment', 'response'].each do |event_type| + incoming_message.info_request_events << InfoRequestEvent.new(:event_type => event_type) + end + incoming_message.response_event.event_type.should == 'response' + end + +end + +describe IncomingMessage, 'when asked if a user can view it' do + + before do + @user = mock_model(User) + @info_request = mock_model(InfoRequest) + @incoming_message = IncomingMessage.new(:info_request => @info_request) + end + + context 'if the prominence is hidden' do + + before do + @incoming_message.prominence = 'hidden' + end + + it 'should return true if the user can view hidden things' do + User.stub!(:view_hidden?).with(@user).and_return(true) + @incoming_message.user_can_view?(@user).should be_true + end + + it 'should return false if the user cannot view hidden things' do + User.stub!(:view_hidden?).with(@user).and_return(false) + @incoming_message.user_can_view?(@user).should be_false + end + + end + + context 'if the prominence is requester_only' do + + before do + @incoming_message.prominence = 'requester_only' + end + + it 'should return true if the user owns the associated request' do + @info_request.stub!(:is_owning_user?).with(@user).and_return(true) + @incoming_message.user_can_view?(@user).should be_true + end + + it 'should return false if the user does not own the associated request' do + @info_request.stub!(:is_owning_user?).with(@user).and_return(false) + @incoming_message.user_can_view?(@user).should be_false + end + end + + context 'if the prominence is normal' do + + before do + @incoming_message.prominence = 'normal' + end + + it 'should return true' do + @incoming_message.user_can_view?(@user).should be_true + end + + end + +end + +describe 'when asked if it is indexed by search' do + + before do + @incoming_message = IncomingMessage.new + end + + it 'should return false if it has prominence "hidden"' do + @incoming_message.prominence = 'hidden' + @incoming_message.indexed_by_search?.should be_false + end + + it 'should return false if it has prominence "requester_only"' do + @incoming_message.prominence = 'requester_only' + @incoming_message.indexed_by_search?.should be_false + end + + it 'should return true if it has prominence "normal"' do + @incoming_message.prominence = 'normal' + @incoming_message.indexed_by_search?.should be_true + end + +end + describe IncomingMessage, " when dealing with incoming mail" do before(:each) do @@ -27,11 +161,11 @@ describe IncomingMessage, " when dealing with incoming mail" do end it "should correctly fold various types of footer" do - Dir.glob(File.join(Spec::Runner.configuration.fixture_path, "files", "email-folding-example-*.txt")).each do |file| + Dir.glob(File.join(RSpec.configuration.fixture_path, "files", "email-folding-example-*.txt")).each do |file| message = File.read(file) parsed = IncomingMessage.remove_quoted_sections(message) expected = File.read("#{file}.expected") - parsed.should include(expected) + parsed.should be_equal_modulo_whitespace_to expected end end @@ -59,12 +193,19 @@ describe IncomingMessage, " when dealing with incoming mail" do message.subject.should == "Câmara Responde: Banco de ideias" end - it 'should not error on display of a message which has no charset set on the body part and - is not good utf-8' do + it 'should deal with GB18030 text even if the charset is missing' do ir = info_requests(:fancy_dog_request) receive_incoming_mail('no-part-charset-bad-utf8.email', ir.incoming_email) message = ir.incoming_messages[1] message.parse_raw_email! + message.get_main_body_text_internal.should include("贵公司负责人") + end + + it 'should not error on display of a message which has no charset set on the body part and is not good UTF-8' do + ir = info_requests(:fancy_dog_request) + receive_incoming_mail('no-part-charset-random-data.email', ir.incoming_email) + message = ir.incoming_messages[1] + message.parse_raw_email! message.get_main_body_text_internal.should include("The above text was badly encoded") end @@ -108,6 +249,16 @@ describe IncomingMessage, " when dealing with incoming mail" do end + it 'should handle a main body part that is just quoted content in an email that has + no subject' do + i = IncomingMessage.new + i.stub!(:get_main_body_text_unfolded).and_return("some quoting") + i.stub!(:get_main_body_text_folded).and_return("FOLDED_QUOTED_SECTION") + i.stub!(:subject).and_return(nil) + i.get_body_for_html_display + end + + end describe IncomingMessage, " display attachments" do @@ -311,7 +462,18 @@ describe IncomingMessage, " when censoring data" do data.should == "His email was x\000x\000x\000@\000x\000x\000x\000.\000x\000x\000x\000, indeed" end - + it 'should handle multibyte characters correctly' do + orig_data = 'á' + data = orig_data.dup + @regex_censor_rule = CensorRule.new() + @regex_censor_rule.text = 'á' + @regex_censor_rule.regexp = true + @regex_censor_rule.replacement = 'cat' + @regex_censor_rule.last_edit_editor = 'unknown' + @regex_censor_rule.last_edit_comment = 'none' + @im.info_request.censor_rules << @regex_censor_rule + lambda{ @im.binary_mask_stuff!(data, "text/plain") }.should_not raise_error + end def pdf_replacement_test(use_ghostscript_compression) config = MySociety::Config.load_default() @@ -353,7 +515,7 @@ describe IncomingMessage, " when censoring data" do end it "should apply hard-coded privacy rules to HTML files" do - data = "http://#{Configuration::domain}/c/cheese" + data = "http://#{AlaveteliConfiguration::domain}/c/cheese" @im.html_mask_stuff!(data) data.should == "[WDTK login link]" end @@ -405,12 +567,24 @@ describe IncomingMessage, " when uudecoding bad messages" do im.stub!(:mail).and_return(mail) im.extract_attachments! + im.reload attachments = im.foi_attachments attachments.size.should == 2 attachments[1].filename.should == 'moo.txt' im.get_attachments_for_display.size.should == 1 end + it "should still work when parsed from the raw email" do + raw_email = load_file_fixture 'inline-uuencode.email' + mail = MailHandler.mail_from_raw_email(raw_email) + im = incoming_messages :useless_incoming_message + im.stub!(:raw_email).and_return(raw_email) + im.stub!(:mail).and_return(mail) + im.parse_raw_email! + attachments = im.foi_attachments + attachments.size.should == 2 + end + it "should apply censor rules" do mail = get_fixture_mail('incoming-request-bad-uuencoding.email') @@ -523,3 +697,58 @@ describe IncomingMessage, "when TNEF attachments are attached to messages" do end end +describe IncomingMessage, "when extracting attachments" do + + before do + load_raw_emails_data + end + + it 'handles the case where reparsing changes the body of the main part + and the cached attachment has been deleted' do + # original set of attachment attributes + attachment_attributes = { :url_part_number => 1, + :within_rfc822_subject => nil, + :content_type => "text/plain", + :charset => nil, + :body => "No way!\n", + :hexdigest => "0c8b1b0f5cb9c94ed15a180e73b5c7d1", + :filename => nil } + + # Make a small change in the body returned for the attachment + new_attachment_attributes = attachment_attributes.merge(:body => "No way!", + :hexdigest => "74d2c0a41e074f9cebe49324d5b47414") + + + # Simulate parsing with the original attachments + MailHandler.stub!(:get_attachment_attributes).and_return([attachment_attributes]) + incoming_message = incoming_messages(:useless_incoming_message) + + # Extract the attachments + incoming_message.extract_attachments! + + # delete the cached file for the main body part + main = incoming_message.get_main_body_text_part + main.delete_cached_file! + + # Simulate reparsing with the slightly changed body + MailHandler.stub!(:get_attachment_attributes).and_return([new_attachment_attributes]) + + # Re-extract the attachments + incoming_message.extract_attachments! + + attachments = incoming_message.foi_attachments + attachments.size.should == 1 + attachments.first.hexdigest.should == "74d2c0a41e074f9cebe49324d5b47414" + attachments.first.body.should == 'No way!' + end + + it 'makes invalid utf-8 encoded attachment text valid when string responds to encode' do + if String.method_defined?(:encode) + im = incoming_messages(:useless_incoming_message) + im.stub!(:extract_text).and_return("\xBF") + + im._get_attachment_text_internal.valid_encoding?.should be_true + end + end + +end diff --git a/spec/models/info_request_batch_spec.rb b/spec/models/info_request_batch_spec.rb new file mode 100644 index 000000000..53158ebe2 --- /dev/null +++ b/spec/models/info_request_batch_spec.rb @@ -0,0 +1,150 @@ +require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') + +describe InfoRequestBatch, "when validating" do + + before do + @info_request_batch = FactoryGirl.build(:info_request_batch) + end + + it 'should require a user' do + @info_request_batch.user = nil + @info_request_batch.valid?.should be_false + @info_request_batch.errors.full_messages.should == ["User can't be blank"] + end + + it 'should require a title' do + @info_request_batch.title = nil + @info_request_batch.valid?.should be_false + @info_request_batch.errors.full_messages.should == ["Title can't be blank"] + end + + it 'should require a body' do + @info_request_batch.body = nil + @info_request_batch.valid?.should be_false + @info_request_batch.errors.full_messages.should == ["Body can't be blank"] + end + +end + +describe InfoRequestBatch, "when finding an existing batch" do + + before do + @first_body = FactoryGirl.create(:public_body) + @second_body = FactoryGirl.create(:public_body) + @info_request_batch = FactoryGirl.create(:info_request_batch, :title => 'Matched title', + :body => 'Matched body', + :public_bodies => [@first_body, + @second_body]) + end + + it 'should return a batch with the same user, title and body sent to one of the same public bodies' do + InfoRequestBatch.find_existing(@info_request_batch.user, + @info_request_batch.title, + @info_request_batch.body, + [@first_body]).should_not be_nil + end + + it 'should not return a batch with the same title and body sent to another public body' do + InfoRequestBatch.find_existing(@info_request_batch.user, + @info_request_batch.title, + @info_request_batch.body, + [FactoryGirl.create(:public_body)]).should be_nil + end + + it 'should not return a batch sent the same public bodies with a different title and body' do + InfoRequestBatch.find_existing(@info_request_batch.user, + 'Other title', + 'Other body', + [@first_body]).should be_nil + end + + it 'should not return a batch sent to one of the same public bodies with the same title and body by + a different user' do + InfoRequestBatch.find_existing(FactoryGirl.create(:user), + @info_request_batch.title, + @info_request_batch.body, + [@first_body]).should be_nil + end +end + +describe InfoRequestBatch, "when creating a batch", :focus => true do + + before do + @title = 'A test title' + @body = "Dear [Authority name],\nA message\nYours faithfully,\nRequester" + @first_public_body = FactoryGirl.create(:public_body) + @second_public_body = FactoryGirl.create(:public_body) + @user = FactoryGirl.create(:user) + @info_request_batch = InfoRequestBatch.create!({:title => @title, + :body => @body, + :public_bodies => [@first_public_body, + @second_public_body], + :user => @user}) + end + + it 'should substitute authority name for the placeholder in each request' do + unrequestable = @info_request_batch.create_batch! + [@first_public_body, @second_public_body].each do |public_body| + request = @info_request_batch.info_requests.detect do |info_request| + info_request.public_body == public_body + end + expected = "Dear #{public_body.name},\nA message\nYours faithfully,\nRequester" + request.outgoing_messages.first.body.should == expected + end + end + + it 'should send requests to requestable public bodies, and return a list of unrequestable ones' do + @first_public_body.stub(:is_requestable?).and_return(false) + unrequestable = @info_request_batch.create_batch! + unrequestable.should == [@first_public_body] + @info_request_batch.info_requests.size.should == 1 + request = @info_request_batch.info_requests.first + request.outgoing_messages.first.status.should == 'sent' + end + + it 'should set the sent_at value of the info request batch' do + @info_request_batch.create_batch! + @info_request_batch.sent_at.should_not be_nil + end + +end + +describe InfoRequestBatch, "when sending batches" do + + before do + @title = 'A test title' + @body = "Dear [Authority name],\nA message\nYours faithfully,\nRequester" + @first_public_body = FactoryGirl.create(:public_body) + @second_public_body = FactoryGirl.create(:public_body) + @user = FactoryGirl.create(:user) + @info_request_batch = InfoRequestBatch.create!({:title => @title, + :body => @body, + :public_bodies => [@first_public_body, + @second_public_body], + :user => @user}) + @sent_batch = InfoRequestBatch.create!({:title => @title, + :body => @body, + :public_bodies => [@first_public_body, + @second_public_body], + :user => @user, + :sent_at => Time.now}) + end + + it 'should send requests and notifications for only unsent batch requests' do + InfoRequestBatch.send_batches + ActionMailer::Base.deliveries.size.should == 3 + first_email = ActionMailer::Base.deliveries.first + first_email.to.should == [@first_public_body.request_email] + first_email.subject.should == 'Freedom of Information request - A test title' + + second_email = ActionMailer::Base.deliveries.second + second_email.to.should == [@second_public_body.request_email] + second_email.subject.should == 'Freedom of Information request - A test title' + + third_email = ActionMailer::Base.deliveries.third + third_email.to.should == [@user.email] + third_email.subject.should == 'Your batch request "A test title" has been sent' + end + +end + diff --git a/spec/models/info_request_event_spec.rb b/spec/models/info_request_event_spec.rb index 796f8b840..53c83bd46 100644 --- a/spec/models/info_request_event_spec.rb +++ b/spec/models/info_request_event_spec.rb @@ -1,3 +1,21 @@ +# coding: utf-8 +# == Schema Information +# +# Table name: info_request_events +# +# id :integer not null, primary key +# info_request_id :integer not null +# event_type :text not null +# params_yaml :text not null +# created_at :datetime not null +# described_state :string(255) +# calculated_state :string(255) +# last_described_at :datetime +# incoming_message_id :integer +# outgoing_message_id :integer +# comment_id :integer +# + require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') describe InfoRequestEvent do @@ -14,6 +32,64 @@ describe InfoRequestEvent do end + describe 'when deciding if it is indexed by search' do + + before do + @comment = mock_model(Comment) + @incoming_message = mock_model(IncomingMessage) + @outgoing_message = mock_model(OutgoingMessage) + @info_request = mock_model(InfoRequest, :indexed_by_search? => true) + end + + it 'should return false for a comment that is not visible' do + @comment.stub!(:visible).and_return(false) + @info_request_event = InfoRequestEvent.new(:event_type => 'comment', + :comment => @comment, + :info_request => @info_request) + @info_request_event.indexed_by_search?.should be_false + end + + it 'should return true for a comment that is visible' do + @comment.stub!(:visible).and_return(true) + @info_request_event = InfoRequestEvent.new(:event_type => 'comment', + :comment => @comment, + :info_request => @info_request) + @info_request_event.indexed_by_search?.should be_true + end + + it 'should return false for an incoming message that is not indexed by search' do + @incoming_message.stub!(:indexed_by_search?).and_return false + @info_request_event = InfoRequestEvent.new(:event_type => 'response', + :incoming_message => @incoming_message, + :info_request => @info_request) + @info_request_event.indexed_by_search?.should be_false + end + + it 'should return true for an incoming message that is indexed by search' do + @incoming_message.stub!(:indexed_by_search?).and_return true + @info_request_event = InfoRequestEvent.new(:event_type => 'response', + :incoming_message => @incoming_message, + :info_request => @info_request) + @info_request_event.indexed_by_search?.should be_true + end + + it 'should return false for an outgoing message that is not indexed by search' do + @outgoing_message.stub!(:indexed_by_search?).and_return false + @info_request_event = InfoRequestEvent.new(:event_type => 'followup_sent', + :outgoing_message => @outgoing_message, + :info_request => @info_request) + @info_request_event.indexed_by_search?.should be_false + end + + it 'should return true for an outgoing message that is indexed by search' do + @outgoing_message.stub!(:indexed_by_search?).and_return true + @info_request_event = InfoRequestEvent.new(:event_type => 'followup_sent', + :outgoing_message => @outgoing_message, + :info_request => @info_request) + @info_request_event.indexed_by_search?.should be_true + end + end + describe 'after saving' do it 'should mark the model for reindexing in xapian if there is no no_xapian_reindex flag on the object' do @@ -21,7 +97,7 @@ describe InfoRequestEvent do :event_type => 'sent', :params => {}) event.should_receive(:xapian_mark_needs_index) - event.run_callbacks(:after_save) + event.run_callbacks(:save) end end @@ -118,6 +194,13 @@ describe InfoRequestEvent do @info_request_event.same_email_as_previous_send?.should be_true end + it 'should handle non-ascii characters in the name input' do + address = "\"Someone’s name\" <test@example.com>" + @info_request_event.stub!(:params).and_return(:email => address) + @info_request_event.stub_chain(:info_request, :get_previous_email_sent_to).and_return(address) + @info_request_event.same_email_as_previous_send?.should be_true + end + end end diff --git a/spec/models/info_request_spec.rb b/spec/models/info_request_spec.rb index 544852f91..9766f928f 100644 --- a/spec/models/info_request_spec.rb +++ b/spec/models/info_request_spec.rb @@ -1,7 +1,66 @@ +# encoding: utf-8 +# == Schema Information +# +# Table name: info_requests +# +# id :integer not null, primary key +# title :text not null +# user_id :integer +# public_body_id :integer not null +# created_at :datetime not null +# updated_at :datetime not null +# described_state :string(255) not null +# awaiting_description :boolean default(FALSE), not null +# prominence :string(255) default("normal"), not null +# url_title :text not null +# law_used :string(255) default("foi"), not null +# allow_new_responses_from :string(255) default("anybody"), not null +# handle_rejected_responses :string(255) default("bounce"), not null +# idhash :string(255) not null +# external_user_name :string(255) +# external_url :string(255) +# attention_requested :boolean default(FALSE) +# comments_allowed :boolean default(TRUE), not null +# + require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') describe InfoRequest do + describe 'when validating' do + + it 'should accept a summary with ascii characters' do + info_request = InfoRequest.new(:title => 'abcde') + info_request.valid? + info_request.errors[:title].should be_empty + end + + it 'should accept a summary with unicode characters' do + info_request = InfoRequest.new(:title => 'кажете') + info_request.valid? + info_request.errors[:title].should be_empty + end + + it 'should not accept a summary with no ascii or unicode characters' do + info_request = InfoRequest.new(:title => '55555') + info_request.valid? + info_request.errors[:title].should_not be_empty + end + + it 'should require a public body id by default' do + info_request = InfoRequest.new + info_request.valid? + info_request.errors[:public_body_id].should_not be_empty + end + + it 'should not require a public body id if it is a batch request template' do + info_request = InfoRequest.new + info_request.is_batch_request_template = true + info_request.valid? + info_request.errors[:public_body_id].should be_empty + end + end + describe 'when generating a user name slug' do before do @@ -156,6 +215,7 @@ describe InfoRequest do end it "should cope with indexing after item is deleted" do + load_raw_emails_data IncomingMessage.find(:all).each{|x| x.parse_raw_email!} rebuild_xapian_index # delete event from underneath indexing; shouldn't cause error @@ -375,10 +435,12 @@ describe InfoRequest do it 'should add extra conditions if supplied' do expected_conditions = ["awaiting_description = ? - AND (SELECT created_at - FROM info_request_events + AND (SELECT info_request_events.created_at + FROM info_request_events, incoming_messages WHERE info_request_events.info_request_id = info_requests.id AND info_request_events.event_type = 'response' + AND incoming_messages.id = info_request_events.incoming_message_id + AND incoming_messages.prominence = 'normal' ORDER BY created_at desc LIMIT 1) < ? AND url_title != 'holding_pen' AND user_id IS NOT NULL @@ -393,21 +455,25 @@ describe InfoRequest do InfoRequest.find_old_unclassified({:conditions => ["prominence != 'backpage'"]}) end - it 'should ask the database for requests that are awaiting description, have a last response older + it 'should ask the database for requests that are awaiting description, have a last public response older than 21 days old, have a user, are not the holding pen and are not backpaged' do expected_conditions = ["awaiting_description = ? - AND (SELECT created_at - FROM info_request_events + AND (SELECT info_request_events.created_at + FROM info_request_events, incoming_messages WHERE info_request_events.info_request_id = info_requests.id AND info_request_events.event_type = 'response' + AND incoming_messages.id = info_request_events.incoming_message_id + AND incoming_messages.prominence = 'normal' ORDER BY created_at desc LIMIT 1) < ? AND url_title != 'holding_pen' AND user_id IS NOT NULL".split(' ').join(' '), true, Time.now - 21.days] - expected_select = "*, (SELECT created_at - FROM info_request_events + expected_select = "*, (SELECT info_request_events.created_at + FROM info_request_events, incoming_messages WHERE info_request_events.info_request_id = info_requests.id AND info_request_events.event_type = 'response' + AND incoming_messages.id = info_request_events.incoming_message_id + AND incoming_messages.prominence = 'normal' ORDER BY created_at desc LIMIT 1) AS last_response_time".split(' ').join(' ') InfoRequest.should_receive(:find) do |all, query_params| @@ -422,12 +488,55 @@ describe InfoRequest do end + describe 'when asked for random old unclassified requests with normal prominence' do + + it "should not return requests that don't have normal prominence" do + dog_request = info_requests(:fancy_dog_request) + old_unclassified = InfoRequest.get_random_old_unclassified(1, :conditions => ["prominence = 'normal'"]) + old_unclassified.length.should == 1 + old_unclassified.first.should == dog_request + dog_request.prominence = 'requester_only' + dog_request.save! + old_unclassified = InfoRequest.get_random_old_unclassified(1, :conditions => ["prominence = 'normal'"]) + old_unclassified.length.should == 0 + dog_request.prominence = 'hidden' + dog_request.save! + old_unclassified = InfoRequest.get_random_old_unclassified(1, :conditions => ["prominence = 'normal'"]) + old_unclassified.length.should == 0 + end + + end + + describe 'when asked to count old unclassified requests with normal prominence' do + + it "should not return requests that don't have normal prominence" do + dog_request = info_requests(:fancy_dog_request) + old_unclassified = InfoRequest.count_old_unclassified(:conditions => ["prominence = 'normal'"]) + old_unclassified.should == 1 + dog_request.prominence = 'requester_only' + dog_request.save! + old_unclassified = InfoRequest.count_old_unclassified(:conditions => ["prominence = 'normal'"]) + old_unclassified.should == 0 + dog_request.prominence = 'hidden' + dog_request.save! + old_unclassified = InfoRequest.count_old_unclassified(:conditions => ["prominence = 'normal'"]) + old_unclassified.should == 0 + end + + end + describe 'when an instance is asked if it is old and unclassified' do before do Time.stub!(:now).and_return(Time.utc(2007, 11, 9, 23, 59)) - @mock_comment_event = safe_mock_model(InfoRequestEvent, :created_at => Time.now - 23.days, :event_type => 'comment') - @mock_response_event = safe_mock_model(InfoRequestEvent, :created_at => Time.now - 22.days, :event_type => 'response') + @mock_comment_event = mock_model(InfoRequestEvent, :created_at => Time.now - 23.days, + :event_type => 'comment', + :response? => false) + mock_incoming_message = mock_model(IncomingMessage, :all_can_view? => true) + @mock_response_event = mock_model(InfoRequestEvent, :created_at => Time.now - 22.days, + :event_type => 'response', + :response? => true, + :incoming_message => mock_incoming_message) @info_request = InfoRequest.new(:prominence => 'normal', :awaiting_description => true, :info_request_events => [@mock_response_event, @mock_comment_event]) @@ -457,16 +566,16 @@ describe InfoRequest do describe 'when applying censor rules' do before do - @global_rule = safe_mock_model(CensorRule, :apply_to_text! => nil, + @global_rule = mock_model(CensorRule, :apply_to_text! => nil, :apply_to_binary! => nil) - @user_rule = safe_mock_model(CensorRule, :apply_to_text! => nil, + @user_rule = mock_model(CensorRule, :apply_to_text! => nil, :apply_to_binary! => nil) - @request_rule = safe_mock_model(CensorRule, :apply_to_text! => nil, + @request_rule = mock_model(CensorRule, :apply_to_text! => nil, :apply_to_binary! => nil) - @body_rule = safe_mock_model(CensorRule, :apply_to_text! => nil, + @body_rule = mock_model(CensorRule, :apply_to_text! => nil, :apply_to_binary! => nil) - @user = safe_mock_model(User, :censor_rules => [@user_rule]) - @body = safe_mock_model(PublicBody, :censor_rules => [@body_rule]) + @user = mock_model(User, :censor_rules => [@user_rule]) + @body = mock_model(PublicBody, :censor_rules => [@body_rule]) @info_request = InfoRequest.new(:prominence => 'normal', :awaiting_description => true, :title => 'title') @@ -504,6 +613,12 @@ describe InfoRequest do @info_request.apply_censor_rules_to_text!(@text) end + it 'should not raise an error if the request is a batch request template' do + @info_request.stub!(:public_body).and_return(nil) + @info_request.is_batch_request_template = true + lambda{ @info_request.apply_censor_rules_to_text!(@text) }.should_not raise_error + end + end context 'when applying censor rules to binary files' do @@ -562,7 +677,619 @@ describe InfoRequest do @info_request.prominence = 'requester_only' @info_request.all_can_view?.should == false end + end + + describe 'when asked for the last public response event' do + + before do + @info_request = FactoryGirl.create(:info_request_with_incoming) + @incoming_message = @info_request.incoming_messages.first + end + + it 'should not return an event with a hidden prominence message' do + @incoming_message.prominence = 'hidden' + @incoming_message.save! + @info_request.get_last_public_response_event.should == nil + end + + it 'should not return an event with a requester_only prominence message' do + @incoming_message.prominence = 'requester_only' + @incoming_message.save! + @info_request.get_last_public_response_event.should == nil + end + it 'should return an event with a normal prominence message' do + @incoming_message.prominence = 'normal' + @incoming_message.save! + @info_request.get_last_public_response_event.should == @incoming_message.response_event + end + end + + describe 'when asked for the last public outgoing event' do + + before do + @info_request = FactoryGirl.create(:info_request) + @outgoing_message = @info_request.outgoing_messages.first + end + + it 'should not return an event with a hidden prominence message' do + @outgoing_message.prominence = 'hidden' + @outgoing_message.save! + @info_request.get_last_public_outgoing_event.should == nil + end + + it 'should not return an event with a requester_only prominence message' do + @outgoing_message.prominence = 'requester_only' + @outgoing_message.save! + @info_request.get_last_public_outgoing_event.should == nil + end + + it 'should return an event with a normal prominence message' do + @outgoing_message.prominence = 'normal' + @outgoing_message.save! + @info_request.get_last_public_outgoing_event.should == @outgoing_message.info_request_events.first + end + + end + + describe 'when asked who can be sent a followup' do + + before do + @info_request = FactoryGirl.create(:info_request_with_plain_incoming) + @incoming_message = @info_request.incoming_messages.first + @public_body = @info_request.public_body + end + + it 'should not include details from a hidden prominence response' do + @incoming_message.prominence = 'hidden' + @incoming_message.save! + @info_request.who_can_followup_to.should == [[@public_body.name, + @public_body.request_email, + nil]] + end + + it 'should not include details from a requester_only prominence response' do + @incoming_message.prominence = 'requester_only' + @incoming_message.save! + @info_request.who_can_followup_to.should == [[@public_body.name, + @public_body.request_email, + nil]] + end + + it 'should include details from a normal prominence response' do + @incoming_message.prominence = 'normal' + @incoming_message.save! + @info_request.who_can_followup_to.should == [[@public_body.name, + @public_body.request_email, + nil], + ['Bob Responder', + "bob@example.com", + @incoming_message.id]] + end + + end + + describe 'when generating json for the api' do + + before do + @user = mock_model(User, :json_for_api => { :id => 20, + :url_name => 'alaveteli_user', + :name => 'Alaveteli User', + :ban_text => '', + :about_me => 'Hi' }) + end + + it 'should return full user info for an internal request' do + @info_request = InfoRequest.new(:user => @user) + @info_request.user_json_for_api.should == { :id => 20, + :url_name => 'alaveteli_user', + :name => 'Alaveteli User', + :ban_text => '', + :about_me => 'Hi' } + end end + describe 'when working out a subject for request emails' do + + it 'should create a standard request subject' do + info_request = FactoryGirl.build(:info_request) + expected_text = "Freedom of Information request - #{info_request.title}" + info_request.email_subject_request.should == expected_text + end + + end + + describe 'when working out a subject for a followup emails' do + + it "should not be confused by an nil subject in the incoming message" do + ir = info_requests(:fancy_dog_request) + im = mock_model(IncomingMessage, + :subject => nil, + :valid_to_reply_to? => true) + subject = ir.email_subject_followup im + subject.should match(/^Re: Freedom of Information request.*fancy dog/) + end + + it "should return a hash with the user's name for an external request" do + @info_request = InfoRequest.new(:external_url => 'http://www.example.com', + :external_user_name => 'External User') + @info_request.user_json_for_api.should == {:name => 'External User'} + end + + it 'should return "Anonymous user" for an anonymous external user' do + @info_request = InfoRequest.new(:external_url => 'http://www.example.com') + @info_request.user_json_for_api.should == {:name => 'Anonymous user'} + end + end + describe "#set_described_state and #log_event" do + context "a request" do + let(:request) { InfoRequest.create!(:title => "my request", + :public_body => public_bodies(:geraldine_public_body), + :user => users(:bob_smith_user)) } + + context "a series of events on a request" do + it "should have sensible events after the initial request has been made" do + # An initial request is sent + # The logic that changes the status when a message is sent is mixed up + # in OutgoingMessage#send_message. So, rather than extract it (or call it) + # let's just duplicate what it does here for the time being. + request.log_event('sent', {}) + request.set_described_state('waiting_response') + + events = request.info_request_events + events.count.should == 1 + events[0].event_type.should == "sent" + events[0].described_state.should == "waiting_response" + events[0].calculated_state.should == "waiting_response" + end + + it "should have sensible events after a response is received to a request" do + # An initial request is sent + request.log_event('sent', {}) + request.set_described_state('waiting_response') + # A response is received + # This is normally done in InfoRequest#receive + request.awaiting_description = true + request.log_event("response", {}) + + events = request.info_request_events + events.count.should == 2 + events[0].event_type.should == "sent" + events[0].described_state.should == "waiting_response" + events[0].calculated_state.should == "waiting_response" + events[1].event_type.should == "response" + events[1].described_state.should be_nil + # TODO: Should calculated_status in this situation be "waiting_classification"? + # This would allow searches like "latest_status: waiting_classification" to be + # available to the user in "Advanced search" + events[1].calculated_state.should be_nil + end + + it "should have sensible events after a request is classified by the requesting user" do + # An initial request is sent + request.log_event('sent', {}) + request.set_described_state('waiting_response') + # A response is received + request.awaiting_description = true + request.log_event("response", {}) + # The request is classified by the requesting user + # This is normally done in RequestController#describe_state + request.log_event("status_update", {}) + request.set_described_state("waiting_response") + + events = request.info_request_events + events.count.should == 3 + events[0].event_type.should == "sent" + events[0].described_state.should == "waiting_response" + events[0].calculated_state.should == "waiting_response" + events[1].event_type.should == "response" + events[1].described_state.should be_nil + events[1].calculated_state.should == 'waiting_response' + events[2].event_type.should == "status_update" + events[2].described_state.should == "waiting_response" + events[2].calculated_state.should == "waiting_response" + end + + it "should have sensible events after a normal followup is sent" do + # An initial request is sent + request.log_event('sent', {}) + request.set_described_state('waiting_response') + # A response is received + request.awaiting_description = true + request.log_event("response", {}) + # The request is classified by the requesting user + request.log_event("status_update", {}) + request.set_described_state("waiting_response") + # A normal follow up is sent + # This is normally done in OutgoingMessage#send_message + request.log_event('followup_sent', {}) + request.set_described_state('waiting_response') + + events = request.info_request_events + events.count.should == 4 + events[0].event_type.should == "sent" + events[0].described_state.should == "waiting_response" + events[0].calculated_state.should == "waiting_response" + events[1].event_type.should == "response" + events[1].described_state.should be_nil + events[1].calculated_state.should == 'waiting_response' + events[2].event_type.should == "status_update" + events[2].described_state.should == "waiting_response" + events[2].calculated_state.should == "waiting_response" + events[3].event_type.should == "followup_sent" + events[3].described_state.should == "waiting_response" + events[3].calculated_state.should == "waiting_response" + end + + it "should have sensible events after a user classifies the request after a follow up" do + # An initial request is sent + request.log_event('sent', {}) + request.set_described_state('waiting_response') + # A response is received + request.awaiting_description = true + request.log_event("response", {}) + # The request is classified by the requesting user + request.log_event("status_update", {}) + request.set_described_state("waiting_response") + # A normal follow up is sent + request.log_event('followup_sent', {}) + request.set_described_state('waiting_response') + # The request is classified by the requesting user + request.log_event("status_update", {}) + request.set_described_state("waiting_response") + + events = request.info_request_events + events.count.should == 5 + events[0].event_type.should == "sent" + events[0].described_state.should == "waiting_response" + events[0].calculated_state.should == "waiting_response" + events[1].event_type.should == "response" + events[1].described_state.should be_nil + events[1].calculated_state.should == 'waiting_response' + events[2].event_type.should == "status_update" + events[2].described_state.should == "waiting_response" + events[2].calculated_state.should == "waiting_response" + events[3].event_type.should == "followup_sent" + events[3].described_state.should == "waiting_response" + events[3].calculated_state.should == "waiting_response" + events[4].event_type.should == "status_update" + events[4].described_state.should == "waiting_response" + events[4].calculated_state.should == "waiting_response" + end + end + + context "another series of events on a request" do + it "should have sensible event states" do + # An initial request is sent + request.log_event('sent', {}) + request.set_described_state('waiting_response') + # An internal review is requested + request.log_event('followup_sent', {}) + request.set_described_state('internal_review') + + events = request.info_request_events + events.count.should == 2 + events[0].event_type.should == "sent" + events[0].described_state.should == "waiting_response" + events[0].calculated_state.should == "waiting_response" + events[1].event_type.should == "followup_sent" + events[1].described_state.should == "internal_review" + events[1].calculated_state.should == "internal_review" + end + + it "should have sensible event states" do + # An initial request is sent + request.log_event('sent', {}) + request.set_described_state('waiting_response') + # An internal review is requested + request.log_event('followup_sent', {}) + request.set_described_state('internal_review') + # The user marks the request as rejected + request.log_event("status_update", {}) + request.set_described_state("rejected") + + events = request.info_request_events + events.count.should == 3 + events[0].event_type.should == "sent" + events[0].described_state.should == "waiting_response" + events[0].calculated_state.should == "waiting_response" + events[1].event_type.should == "followup_sent" + events[1].described_state.should == "internal_review" + events[1].calculated_state.should == "internal_review" + events[2].event_type.should == "status_update" + events[2].described_state.should == "rejected" + events[2].calculated_state.should == "rejected" + end + end + + context "another series of events on a request" do + it "should have sensible event states" do + # An initial request is sent + request.log_event('sent', {}) + request.set_described_state('waiting_response') + # The user marks the request as successful (I know silly but someone did + # this in https://www.whatdotheyknow.com/request/family_support_worker_redundanci) + request.log_event("status_update", {}) + request.set_described_state("successful") + + events = request.info_request_events + events.count.should == 2 + events[0].event_type.should == "sent" + events[0].described_state.should == "waiting_response" + events[0].calculated_state.should == "waiting_response" + events[1].event_type.should == "status_update" + events[1].described_state.should == "successful" + events[1].calculated_state.should == "successful" + end + + it "should have sensible event states" do + # An initial request is sent + request.log_event('sent', {}) + request.set_described_state('waiting_response') + + # A response is received + request.awaiting_description = true + request.log_event("response", {}) + + # The user marks the request as successful + request.log_event("status_update", {}) + request.set_described_state("successful") + + events = request.info_request_events + events.count.should == 3 + events[0].event_type.should == "sent" + events[0].described_state.should == "waiting_response" + events[0].calculated_state.should == "waiting_response" + events[1].event_type.should == "response" + events[1].described_state.should be_nil + events[1].calculated_state.should == "successful" + events[2].event_type.should == "status_update" + events[2].described_state.should == "successful" + events[2].calculated_state.should == "successful" + end + end + + context "another series of events on a request" do + it "should have sensible event states" do + # An initial request is sent + request.log_event('sent', {}) + request.set_described_state('waiting_response') + # An admin sets the status of the request to 'gone postal' using + # the admin interface + request.log_event("edit", {}) + request.set_described_state("gone_postal") + + events = request.info_request_events + events.count.should == 2 + events[0].event_type.should == "sent" + events[0].described_state.should == "waiting_response" + events[0].calculated_state.should == "waiting_response" + events[1].event_type.should == "edit" + events[1].described_state.should == "gone_postal" + events[1].calculated_state.should == "gone_postal" + end + end + end + end + + describe 'when saving an info_request' do + + before do + @info_request = InfoRequest.new(:external_url => 'http://www.example.com', + :external_user_name => 'Example User', + :title => 'Some request or other', + :public_body => public_bodies(:geraldine_public_body)) + end + + it "should call purge_in_cache and update_counter_cache" do + @info_request.should_receive(:purge_in_cache) + # Twice - once for save, once for destroy: + @info_request.should_receive(:update_counter_cache).twice + @info_request.save! + @info_request.destroy + end + + end + + describe 'when destroying an info_request' do + + before do + @info_request = InfoRequest.new(:external_url => 'http://www.example.com', + :external_user_name => 'Example User', + :title => 'Some request or other', + :public_body => public_bodies(:geraldine_public_body)) + end + + it "should call update_counter_cache" do + @info_request.save! + @info_request.should_receive(:update_counter_cache) + @info_request.destroy + end + + end + + describe 'when changing a described_state' do + + it "should change the counts on its PublicBody without saving a new version" do + pb = public_bodies(:geraldine_public_body) + old_version_count = pb.versions.count + old_successful_count = pb.info_requests_successful_count + old_not_held_count = pb.info_requests_not_held_count + ir = InfoRequest.new(:external_url => 'http://www.example.com', + :external_user_name => 'Example User', + :title => 'Some request or other', + :described_state => 'partially_successful', + :public_body => pb) + ir.save! + pb.info_requests_successful_count.should == (old_successful_count + 1) + ir.described_state = 'not_held' + ir.save! + pb.reload + pb.info_requests_successful_count.should == old_successful_count + pb.info_requests_not_held_count.should == (old_not_held_count + 1) + ir.described_state = 'successful' + ir.save! + pb.reload + pb.info_requests_successful_count.should == (old_successful_count + 1) + pb.info_requests_not_held_count.should == old_not_held_count + ir.destroy + pb.reload + pb.info_requests_successful_count.should == old_successful_count + pb.info_requests_successful_count.should == old_not_held_count + pb.versions.count.should == old_version_count + end + + end + + describe InfoRequest, 'when getting similar requests' do + + before(:each) do + get_fixtures_xapian_index + end + + it 'should return similar requests' do + similar, more = info_requests(:spam_1_request).similar_requests(1) + similar.results.first[:model].info_request.should == info_requests(:spam_2_request) + end + + it 'should return a flag set to true' do + similar, more = info_requests(:spam_1_request).similar_requests(1) + more.should be_true + end + + end + + describe InfoRequest, 'when constructing the list of recent requests' do + + before(:each) do + get_fixtures_xapian_index + end + + describe 'when there are fewer than five successful requests' do + + it 'should list the most recently sent and successful requests by the creation date of the + request event' do + # Make sure the newest response is listed first even if a request + # with an older response has a newer comment or was reclassified more recently: + # https://github.com/mysociety/alaveteli/issues/370 + # + # This is a deliberate behaviour change, in that the + # previous behaviour (showing more-recently-reclassified + # requests first) was intentional. + request_events, request_events_all_successful = InfoRequest.recent_requests + previous = nil + request_events.each do |event| + if previous + previous.created_at.should be >= event.created_at + end + ['sent', 'response'].include?(event.event_type).should be_true + if event.event_type == 'response' + ['successful', 'partially_successful'].include?(event.calculated_state).should be_true + end + previous = event + end + end + end + + it 'should coalesce duplicate requests' do + request_events, request_events_all_successful = InfoRequest.recent_requests + request_events.map(&:info_request).select{|x|x.url_title =~ /^spam/}.length.should == 1 + end + end + + describe InfoRequest, "when constructing a list of requests by query" do + + before(:each) do + get_fixtures_xapian_index + end + + def apply_filters(filters) + results = InfoRequest.request_list(filters, page=1, per_page=100, max_results=100) + results[:results].map(&:info_request) + end + + it "should filter requests" do + apply_filters(:latest_status => 'all').should =~ InfoRequest.all + + # default sort order is the request with the most recently created event first + apply_filters(:latest_status => 'all').should == InfoRequest.all( + :order => "(SELECT max(info_request_events.created_at) + FROM info_request_events + WHERE info_request_events.info_request_id = info_requests.id) + DESC") + + apply_filters(:latest_status => 'successful').should =~ InfoRequest.all( + :conditions => "id in ( + SELECT info_request_id + FROM info_request_events + WHERE NOT EXISTS ( + SELECT * + FROM info_request_events later_events + WHERE later_events.created_at > info_request_events.created_at + AND later_events.info_request_id = info_request_events.info_request_id + AND later_events.described_state IS NOT null + ) + AND info_request_events.described_state IN ('successful', 'partially_successful') + )") + + end + + it "should filter requests by date" do + # The semantics of the search are that it finds any InfoRequest + # that has any InfoRequestEvent created in the specified range + filters = {:latest_status => 'all', :request_date_before => '13/10/2007'} + apply_filters(filters).should =~ InfoRequest.all( + :conditions => "id IN (SELECT info_request_id + FROM info_request_events + WHERE created_at < '2007-10-13'::date)") + + filters = {:latest_status => 'all', :request_date_after => '13/10/2007'} + apply_filters(filters).should =~ InfoRequest.all( + :conditions => "id IN (SELECT info_request_id + FROM info_request_events + WHERE created_at > '2007-10-13'::date)") + + filters = {:latest_status => 'all', + :request_date_after => '13/10/2007', + :request_date_before => '01/11/2007'} + apply_filters(filters).should =~ InfoRequest.all( + :conditions => "id IN (SELECT info_request_id + FROM info_request_events + WHERE created_at BETWEEN '2007-10-13'::date + AND '2007-11-01'::date)") + end + + + it "should list internal_review requests as unresolved ones" do + + # This doesn’t precisely duplicate the logic of the actual + # query, but it is close enough to give the same result with + # the current set of test data. + results = apply_filters(:latest_status => 'awaiting') + results.should =~ InfoRequest.all( + :conditions => "id IN (SELECT info_request_id + FROM info_request_events + WHERE described_state in ( + 'waiting_response', 'waiting_clarification', + 'internal_review', 'gone_postal', 'error_message', 'requires_admin' + ) and not exists ( + select * + from info_request_events later_events + where later_events.created_at > info_request_events.created_at + and later_events.info_request_id = info_request_events.info_request_id + ))") + + + results.include?(info_requests(:fancy_dog_request)).should == false + + event = info_request_events(:useless_incoming_message_event) + event.described_state = event.calculated_state = "internal_review" + event.save! + rebuild_xapian_index + results = apply_filters(:latest_status => 'awaiting') + results.include?(info_requests(:fancy_dog_request)).should == true + end + + + end end diff --git a/spec/models/mail_server_log_spec.rb b/spec/models/mail_server_log_spec.rb index d0a1d202f..67709b130 100644 --- a/spec/models/mail_server_log_spec.rb +++ b/spec/models/mail_server_log_spec.rb @@ -1,9 +1,22 @@ +# == Schema Information +# +# Table name: mail_server_logs +# +# id :integer not null, primary key +# mail_server_log_done_id :integer +# info_request_id :integer +# order :integer not null +# line :text not null +# created_at :datetime not null +# updated_at :datetime not null +# + require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') describe MailServerLog do describe ".load_file" do before :each do - Configuration.stub!(:incoming_email_domain).and_return("example.com") + AlaveteliConfiguration.stub!(:incoming_email_domain).and_return("example.com") File.stub_chain(:stat, :mtime).and_return(DateTime.new(2012, 10, 10)) end @@ -64,8 +77,8 @@ describe MailServerLog do describe ".email_addresses_on_line" do before :each do - Configuration.stub!(:incoming_email_domain).and_return("example.com") - Configuration.stub!(:incoming_email_prefix).and_return("foi+") + AlaveteliConfiguration.stub!(:incoming_email_domain).and_return("example.com") + AlaveteliConfiguration.stub!(:incoming_email_prefix).and_return("foi+") end it "recognises a single incoming email" do @@ -106,7 +119,7 @@ describe MailServerLog do # Postfix logs for a single email go over multiple lines. They are all tied together with the Queue ID. # See http://onlamp.com/onlamp/2004/01/22/postfix.html it "loads the postfix log and untangles seperate email transactions using the queue ID" do - Configuration.stub!(:incoming_email_domain).and_return("example.com") + AlaveteliConfiguration.stub!(:incoming_email_domain).and_return("example.com") log.stub!(:rewind) ir1 = info_requests(:fancy_dog_request) ir2 = info_requests(:naughty_chicken_request) @@ -135,7 +148,7 @@ describe MailServerLog do describe ".scan_for_postfix_queue_ids" do it "returns the queue ids of interest with the connected email addresses" do - Configuration.stub!(:incoming_email_domain).and_return("example.com") + AlaveteliConfiguration.stub!(:incoming_email_domain).and_return("example.com") MailServerLog.scan_for_postfix_queue_ids(log).should == { "CB55836EE58C" => ["request-14-e0e09f97@example.com"], "9634B16F7F7" => ["request-10-1234@example.com"] diff --git a/spec/models/outgoing_mailer_spec.rb b/spec/models/outgoing_mailer_spec.rb deleted file mode 100644 index 5d1ea2dfb..000000000 --- a/spec/models/outgoing_mailer_spec.rb +++ /dev/null @@ -1,140 +0,0 @@ -require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') - -describe OutgoingMailer, " when working out follow up addresses" do - # This is done with fixtures as the code is a bit tangled with the way it - # calls TMail. XXX untangle it and make these tests spread out and using - # mocks. Put parts of the tests in spec/lib/tmail_extensions.rb - before(:each) do - load_raw_emails_data - end - - it "should parse them right" do - ir = info_requests(:fancy_dog_request) - im = ir.incoming_messages[0] - - # check the basic entry in the fixture is fine - OutgoingMailer.name_and_email_for_followup(ir, im).should == "FOI Person <foiperson@localhost>" - OutgoingMailer.name_for_followup(ir, im).should == "FOI Person" - OutgoingMailer.email_for_followup(ir, im).should == "foiperson@localhost" - end - - it "should work when there is only an email address" do - ir = info_requests(:fancy_dog_request) - im = ir.incoming_messages[0] - - im.raw_email.data = im.raw_email.data.sub("\"FOI Person\" <foiperson@localhost>", "foiperson@localhost") - im.parse_raw_email! true - - # check the basic entry in the fixture is fine - OutgoingMailer.name_and_email_for_followup(ir, im).should == "foiperson@localhost" - OutgoingMailer.name_for_followup(ir, im).should == "Geraldine Quango" - OutgoingMailer.email_for_followup(ir, im).should == "foiperson@localhost" - end - - it "should quote funny characters" do - ir = info_requests(:fancy_dog_request) - im = ir.incoming_messages[0] - - im.raw_email.data = im.raw_email.data.sub("FOI Person", "FOI [ Person") - im.parse_raw_email! true - - # check the basic entry in the fixture is fine - OutgoingMailer.name_and_email_for_followup(ir, im).should == "\"FOI [ Person\" <foiperson@localhost>" - OutgoingMailer.name_for_followup(ir, im).should == "FOI [ Person" - OutgoingMailer.email_for_followup(ir, im).should == "foiperson@localhost" - end - - it "should quote quotes" do - ir = info_requests(:fancy_dog_request) - im = ir.incoming_messages[0] - - im.raw_email.data = im.raw_email.data.sub("FOI Person", "FOI \\\" Person") - im.parse_raw_email! true - - # check the basic entry in the fixture is fine - OutgoingMailer.name_and_email_for_followup(ir, im).should == "\"FOI \\\" Person\" <foiperson@localhost>" - OutgoingMailer.name_for_followup(ir, im).should == "FOI \" Person" - OutgoingMailer.email_for_followup(ir, im).should == "foiperson@localhost" - end - - it "should quote @ signs" do - ir = info_requests(:fancy_dog_request) - im = ir.incoming_messages[0] - - im.raw_email.data = im.raw_email.data.sub("FOI Person", "FOI @ Person") - im.parse_raw_email! true - - # check the basic entry in the fixture is fine - OutgoingMailer.name_and_email_for_followup(ir, im).should == "\"FOI @ Person\" <foiperson@localhost>" - OutgoingMailer.name_for_followup(ir, im).should == "FOI @ Person" - OutgoingMailer.email_for_followup(ir, im).should == "foiperson@localhost" - end - -end - -describe OutgoingMailer, "when working out follow up subjects" do - - before(:each) do - load_raw_emails_data - end - - it "should prefix the title with 'Freedom of Information request -' for initial requests" do - ir = info_requests(:fancy_dog_request) - im = ir.incoming_messages[0] - - ir.email_subject_request.should == "Freedom of Information request - Why do you have & such a fancy dog?" - end - - it "should use 'Re:' and inital request subject for followups which aren't replies to particular messages" do - ir = info_requests(:fancy_dog_request) - om = outgoing_messages(:useless_outgoing_message) - - OutgoingMailer.subject_for_followup(ir, om).should == "Re: Freedom of Information request - Why do you have & such a fancy dog?" - end - - it "should prefix with Re: the subject of the message being replied to" do - ir = info_requests(:fancy_dog_request) - im = ir.incoming_messages[0] - om = outgoing_messages(:useless_outgoing_message) - om.incoming_message_followup = im - - OutgoingMailer.subject_for_followup(ir, om).should == "Re: Geraldine FOI Code AZXB421" - end - - it "should not add Re: prefix if there already is such a prefix" do - ir = info_requests(:fancy_dog_request) - im = ir.incoming_messages[0] - om = outgoing_messages(:useless_outgoing_message) - om.incoming_message_followup = im - - im.raw_email.data = im.raw_email.data.sub("Subject: Geraldine FOI Code AZXB421", "Subject: Re: Geraldine FOI Code AZXB421") - OutgoingMailer.subject_for_followup(ir, om).should == "Re: Geraldine FOI Code AZXB421" - end - - it "should not add Re: prefix if there already is a lower case re: prefix" do - ir = info_requests(:fancy_dog_request) - im = ir.incoming_messages[0] - om = outgoing_messages(:useless_outgoing_message) - om.incoming_message_followup = im - - im.raw_email.data = im.raw_email.data.sub("Subject: Geraldine FOI Code AZXB421", "Subject: re: Geraldine FOI Code AZXB421") - im.parse_raw_email! true - - OutgoingMailer.subject_for_followup(ir, om).should == "re: Geraldine FOI Code AZXB421" - end - - it "should use 'Re:' and initial request subject when replying to failed delivery notifications" do - ir = info_requests(:fancy_dog_request) - im = ir.incoming_messages[0] - om = outgoing_messages(:useless_outgoing_message) - om.incoming_message_followup = im - - im.raw_email.data = im.raw_email.data.sub("foiperson@localhost", "postmaster@localhost") - im.raw_email.data = im.raw_email.data.sub("Subject: Geraldine FOI Code AZXB421", "Subject: Delivery Failed") - im.parse_raw_email! true - - OutgoingMailer.subject_for_followup(ir, om).should == "Re: Freedom of Information request - Why do you have & such a fancy dog?" - end -end - - diff --git a/spec/models/outgoing_message_spec.rb b/spec/models/outgoing_message_spec.rb index 51bb6fdf5..a3e2d1c68 100644 --- a/spec/models/outgoing_message_spec.rb +++ b/spec/models/outgoing_message_spec.rb @@ -1,3 +1,21 @@ +# == Schema Information +# +# Table name: outgoing_messages +# +# id :integer not null, primary key +# info_request_id :integer not null +# body :text not null +# status :string(255) not null +# message_type :string(255) not null +# created_at :datetime not null +# updated_at :datetime not null +# last_sent_at :datetime +# incoming_message_followup_id :integer +# what_doing :string(255) not null +# prominence :string(255) default("normal"), not null +# prominence_reason :text +# + require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') describe OutgoingMessage, " when making an outgoing message" do @@ -16,7 +34,7 @@ describe OutgoingMessage, " when making an outgoing message" do it "should not index the email addresses" do # also used for track emails @outgoing_message.get_text_for_indexing.should_not include("foo@bar.com") - end + end it "should not display email addresses on page" do @outgoing_message.get_body_for_html_display.should_not include("foo@bar.com") @@ -33,10 +51,112 @@ describe OutgoingMessage, " when making an outgoing message" do it "should work out a salutation" do @om.get_salutation.should == "Dear Geraldine Quango," end + + it 'should produce the expected text for an internal review request' do + public_body = mock_model(PublicBody, :name => 'A test public body') + info_request = mock_model(InfoRequest, :public_body => public_body, + :url_title => 'a_test_title', + :title => 'A test title', + :apply_censor_rules_to_text! => nil, + :is_batch_request_template? => false) + outgoing_message = OutgoingMessage.new({ + :status => 'ready', + :message_type => 'followup', + :what_doing => 'internal_review', + :info_request => info_request + }) + expected_text = "I am writing to request an internal review of A test public body's handling of my FOI request 'A test title'." + outgoing_message.body.should include(expected_text) + end + + context "when associated with a batch template request" do + + it 'should produce a salutation with a placeholder' do + @om.info_request.is_batch_request_template = true + @om.get_salutation.should == 'Dear [Authority name],' + end + end + + + describe 'when asked if a user can view it' do + + before do + @info_request = FactoryGirl.create(:info_request) + @outgoing_message = @info_request.outgoing_messages.first + end + + context 'if the prominence is hidden' do + + before do + @outgoing_message.prominence = 'hidden' + end + + it 'should return true for an admin user' do + @outgoing_message.user_can_view?(FactoryGirl.create(:admin_user)).should be_true + end + + it 'should return false for a non-admin user' do + @outgoing_message.user_can_view?(FactoryGirl.create(:user)).should be_false + end + + end + + context 'if the prominence is requester_only' do + + before do + @outgoing_message.prominence = 'requester_only' + end + + it 'should return true if the user owns the associated request' do + @outgoing_message.user_can_view?(@info_request.user).should be_true + end + + it 'should return false if the user does not own the associated request' do + @outgoing_message.user_can_view?(FactoryGirl.create(:user)).should be_false + end + end + + context 'if the prominence is normal' do + + before do + @outgoing_message.prominence = 'normal' + end + + it 'should return true for a non-admin user' do + @outgoing_message.user_can_view?(FactoryGirl.create(:user)).should be_true + end + + end + + end + + describe 'when asked if it is indexed by search' do + + before do + @info_request = FactoryGirl.create(:info_request) + @outgoing_message = @info_request.outgoing_messages.first + end + + it 'should return false if it has prominence "hidden"' do + @outgoing_message.prominence = 'hidden' + @outgoing_message.indexed_by_search?.should be_false + end + + it 'should return false if it has prominence "requester_only"' do + @outgoing_message.prominence = 'requester_only' + @outgoing_message.indexed_by_search?.should be_false + end + + it 'should return true if it has prominence "normal"' do + @outgoing_message.prominence = 'normal' + @outgoing_message.indexed_by_search?.should be_true + end + + end end -describe IncomingMessage, " when censoring data" do +describe OutgoingMessage, " when censoring data" do before do @om = outgoing_messages(:useless_outgoing_message) @@ -56,4 +176,12 @@ describe IncomingMessage, " when censoring data" do end end +describe OutgoingMessage, "when validating the format of the message body" do + + it 'should handle a salutation with a bracket in it' do + outgoing_message = FactoryGirl.build(:initial_request) + outgoing_message.stub!(:get_salutation).and_return("Dear Bob (Robert,") + lambda{ outgoing_message.valid? }.should_not raise_error(RegexpError) + end +end diff --git a/spec/models/post_redirect_spec.rb b/spec/models/post_redirect_spec.rb index 5f51b6de5..73740e914 100644 --- a/spec/models/post_redirect_spec.rb +++ b/spec/models/post_redirect_spec.rb @@ -1,3 +1,19 @@ +# == Schema Information +# +# Table name: post_redirects +# +# id :integer not null, primary key +# token :text not null +# uri :text not null +# post_params_yaml :text +# created_at :datetime not null +# updated_at :datetime not null +# email_token :text not null +# reason_params_yaml :text +# user_id :integer +# circumstance :text default("normal"), not null +# + require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') describe PostRedirect, " when constructing" do diff --git a/spec/models/profile_photo_spec.rb b/spec/models/profile_photo_spec.rb index 892cccd08..e70f474a0 100644 --- a/spec/models/profile_photo_spec.rb +++ b/spec/models/profile_photo_spec.rb @@ -1,11 +1,21 @@ +# == Schema Information +# +# Table name: profile_photos +# +# id :integer not null, primary key +# data :binary not null +# user_id :integer +# draft :boolean default(FALSE), not null +# + require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') -describe ProfilePhoto, "when constructing a new photo" do +describe ProfilePhoto, "when constructing a new photo" do - before do + before do @mock_user = mock_model(User) end - + it 'should take no image as invalid' do profile_photo = ProfilePhoto.new(:data => nil, :user => @mock_user) profile_photo.valid?.should == false @@ -16,7 +26,15 @@ describe ProfilePhoto, "when constructing a new photo" do profile_photo.valid?.should == false end - it 'should accept and convert a PNG to right size' do + it 'should translate a no image error message' do + I18n.with_locale(:es) do + profile_photo = ProfilePhoto.new(:data => nil, :user => @mock_user) + profile_photo.valid?.should == false + profile_photo.errors[:data].should == ['Por favor elige el fichero que contiene tu foto'] + end + end + + it 'should accept and convert a PNG to right size' do data = load_file_fixture("parrot.png") profile_photo = ProfilePhoto.new(:data => data, :user => @mock_user) profile_photo.valid?.should == true @@ -25,7 +43,7 @@ describe ProfilePhoto, "when constructing a new photo" do profile_photo.image.rows.should == 96 end - it 'should accept and convert a JPEG to right format and size' do + it 'should accept and convert a JPEG to right format and size' do data = load_file_fixture("parrot.jpg") profile_photo = ProfilePhoto.new(:data => data, :user => @mock_user) profile_photo.valid?.should == true @@ -34,7 +52,7 @@ describe ProfilePhoto, "when constructing a new photo" do profile_photo.image.rows.should == 96 end - it 'should accept a draft PNG and not resize it' do + it 'should accept a draft PNG and not resize it' do data = load_file_fixture("parrot.png") profile_photo = ProfilePhoto.new(:data => data, :draft => true) profile_photo.valid?.should == true @@ -43,6 +61,6 @@ describe ProfilePhoto, "when constructing a new photo" do profile_photo.image.rows.should == 289 end - + end diff --git a/spec/models/public_body_change_request_spec.rb b/spec/models/public_body_change_request_spec.rb new file mode 100644 index 000000000..0c4cea67b --- /dev/null +++ b/spec/models/public_body_change_request_spec.rb @@ -0,0 +1,139 @@ +# == Schema Information +# +# Table name: public_body_change_requests +# +# id :integer not null, primary key +# user_email :string(255) +# user_name :string(255) +# user_id :integer +# public_body_name :text +# public_body_id :integer +# public_body_email :string(255) +# source_url :text +# notes :text +# is_open :boolean default(TRUE), not null +# created_at :datetime not null +# updated_at :datetime not null +# + +require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') + +describe PublicBodyChangeRequest, 'when validating' do + + it 'should not be valid without a public body name' do + change_request = PublicBodyChangeRequest.new() + change_request.valid?.should be_false + change_request.errors[:public_body_name].should == ['Please enter the name of the authority'] + end + + it 'should not be valid without a user name if there is no user' do + change_request = PublicBodyChangeRequest.new(:public_body_name => 'New Body') + change_request.valid?.should be_false + change_request.errors[:user_name].should == ['Please enter your name'] + end + + it 'should not be valid without a user email address if there is no user' do + change_request = PublicBodyChangeRequest.new(:public_body_name => 'New Body') + change_request.valid?.should be_false + change_request.errors[:user_email].should == ['Please enter your email address'] + end + + it 'should be valid with a user and no name or email address' do + user = FactoryGirl.build(:user) + change_request = PublicBodyChangeRequest.new(:user => user, + :public_body_name => 'New Body') + change_request.valid?.should be_true + end + + it 'should validate the format of a user email address entered' do + change_request = PublicBodyChangeRequest.new(:public_body_name => 'New Body', + :user_email => '@example.com') + change_request.valid?.should be_false + change_request.errors[:user_email].should == ["Your email doesn't look like a valid address"] + end + + it 'should validate the format of a public body email address entered' do + change_request = PublicBodyChangeRequest.new(:public_body_name => 'New Body', + :public_body_email => '@example.com') + change_request.valid?.should be_false + change_request.errors[:public_body_email].should == ["The authority email doesn't look like a valid address"] + end + +end + +describe PublicBodyChangeRequest, 'get_user_name' do + + it 'should return the user_name field if there is no user association' do + change_request = PublicBodyChangeRequest.new(:user_name => 'Test User') + change_request.get_user_name.should == 'Test User' + end + + it 'should return the name of the associated user if there is one' do + user = FactoryGirl.build(:user) + change_request = PublicBodyChangeRequest.new(:user => user) + change_request.get_user_name.should == user.name + end + +end + + +describe PublicBodyChangeRequest, 'get_user_email' do + + it 'should return the user_email field if there is no user association' do + change_request = PublicBodyChangeRequest.new(:user_email => 'user@example.com') + change_request.get_user_email.should == 'user@example.com' + end + + it 'should return the email of the associated user if there is one' do + user = FactoryGirl.build(:user) + change_request = PublicBodyChangeRequest.new(:user => user) + change_request.get_user_email.should == user.email + end + +end + + +describe PublicBodyChangeRequest, 'get_public_body_name' do + + it 'should return the public_body_name field if there is no public body association' do + change_request = PublicBodyChangeRequest.new(:public_body_name => 'Test Authority') + change_request.get_public_body_name.should == 'Test Authority' + end + + it 'should return the name of the associated public body if there is one' do + public_body = FactoryGirl.build(:public_body) + change_request = PublicBodyChangeRequest.new(:public_body => public_body) + change_request.get_public_body_name.should == public_body.name + end + +end + +describe PublicBodyChangeRequest, 'when creating a comment for the associated public body' do + + it 'should include requesting user, source_url and notes' do + change_request = PublicBodyChangeRequest.new(:user_name => 'Test User', + :user_email => 'test@example.com', + :source_url => 'http://www.example.com', + :notes => 'Some notes') + expected = "Requested by: Test User (test@example.com)\nSource URL: http://www.example.com\nNotes: Some notes" + change_request.comment_for_public_body.should == expected + end + +end + +describe PublicBodyChangeRequest, 'when creating a default subject for a response email' do + + it 'should create an appropriate subject for a request to add a body' do + change_request = PublicBodyChangeRequest.new(:public_body_name => 'Test Body') + change_request.default_response_subject.should == 'Your request to add Test Body to Alaveteli' + end + + it 'should create an appropriate subject for a request to update an email address' do + public_body = FactoryGirl.build(:public_body) + change_request = PublicBodyChangeRequest.new(:public_body => public_body) + change_request.default_response_subject.should == "Your request to update #{public_body.name} on Alaveteli" + + end + +end + diff --git a/spec/models/public_body_spec.rb b/spec/models/public_body_spec.rb index c2e0a6353..dc09bdfa6 100644 --- a/spec/models/public_body_spec.rb +++ b/spec/models/public_body_spec.rb @@ -1,3 +1,31 @@ +# encoding: UTF-8 +# == Schema Information +# +# Table name: public_bodies +# +# id :integer not null, primary key +# name :text not null +# short_name :text not null +# request_email :text not null +# version :integer not null +# last_edit_editor :string(255) not null +# last_edit_comment :text not null +# created_at :datetime not null +# updated_at :datetime not null +# url_name :text not null +# home_page :text default(""), not null +# notes :text default(""), not null +# first_letter :string(255) not null +# publication_scheme :text default(""), not null +# api_key :string(255) not null +# info_requests_count :integer default(0), not null +# disclosure_log :text default(""), not null +# info_requests_successful_count :integer +# info_requests_not_held_count :integer +# info_requests_overdue_count :integer +# info_requests_visible_classified_count :integer +# + require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') describe PublicBody, " using tags" do @@ -135,40 +163,84 @@ describe PublicBody, " when saving" do @public_body = PublicBody.new end + def set_default_attributes(public_body) + public_body.name = "Testing Public Body" + public_body.short_name = "TPB" + public_body.request_email = "request@localhost" + public_body.last_edit_editor = "*test*" + public_body.last_edit_comment = "This is a test" + end + it "should not be valid without setting some parameters" do @public_body.should_not be_valid end it "should not be valid with misformatted request email" do - @public_body.name = "Testing Public Body" - @public_body.short_name = "TPB" + set_default_attributes(@public_body) @public_body.request_email = "requestBOOlocalhost" - @public_body.last_edit_editor = "*test*" - @public_body.last_edit_comment = "This is a test" @public_body.should_not be_valid @public_body.should have(1).errors_on(:request_email) end it "should save" do - @public_body.name = "Testing Public Body" - @public_body.short_name = "TPB" - @public_body.request_email = "request@localhost" - @public_body.last_edit_editor = "*test*" - @public_body.last_edit_comment = "This is a test" + set_default_attributes(@public_body) @public_body.save! end it "should update first_letter" do - @public_body.name = "Testing Public Body" - @public_body.short_name = "TPB" - @public_body.request_email = "request@localhost" - @public_body.last_edit_editor = "*test*" - @public_body.last_edit_comment = "This is a test" - + set_default_attributes(@public_body) @public_body.first_letter.should be_nil @public_body.save! @public_body.first_letter.should == 'T' end + + it "should update first letter, even if it's a multibyte character" do + pb = PublicBody.new(:name => 'åccents, lower-case', + :short_name => 'ALC', + :request_email => 'foo@localhost', + :last_edit_editor => 'test', + :last_edit_comment => '') + pb.first_letter.should be_nil + pb.save! + pb.first_letter.should == 'Å' + end + + it "should save the name when renaming an existing public body" do + public_body = public_bodies(:geraldine_public_body) + public_body.name = "Mark's Public Body" + public_body.save! + + public_body.name.should == "Mark's Public Body" + end + + it 'should update the right translation when in a locale with an underscore' do + AlaveteliLocalization.set_locales('he_IL', 'he_IL') + public_body = public_bodies(:humpadink_public_body) + translation_count = public_body.translations.size + public_body.name = 'Renamed' + public_body.save! + public_body.translations.size.should == translation_count + end + + it 'should not create a new version when nothing has changed' do + @public_body.versions.size.should == 0 + set_default_attributes(@public_body) + @public_body.save! + @public_body.versions.size.should == 1 + @public_body.save! + @public_body.versions.size.should == 1 + end + + it 'should create a new version if something has changed' do + @public_body.versions.size.should == 0 + set_default_attributes(@public_body) + @public_body.save! + @public_body.versions.size.should == 1 + @public_body.name = 'Test' + @public_body.save! + @public_body.versions.size.should == 2 + end + end describe PublicBody, "when searching" do @@ -210,7 +282,7 @@ describe PublicBody, "when searching" do end it "should cope with same url_name across multiple locales" do - PublicBody.with_locale(:es) do + I18n.with_locale(:es) do # use the unique spanish name to retrieve and edit body = PublicBody.find_by_url_name_with_historic('etgq') body.short_name = 'tgq' # Same as english version @@ -228,10 +300,41 @@ describe PublicBody, "when searching" do end end +describe PublicBody, "when asked for the internal_admin_body" do + before(:each) do + # Make sure that there's no internal_admin_body before each of + # these tests: + PublicBody.connection.delete("DELETE FROM public_bodies WHERE url_name = 'internal_admin_body'") + PublicBody.connection.delete("DELETE FROM public_body_translations WHERE url_name = 'internal_admin_body'") + end + + it "should create the internal_admin_body if it didn't exist" do + iab = PublicBody.internal_admin_body + iab.should_not be_nil + end + + it "should find the internal_admin_body even if the default locale has changed since it was created" do + with_default_locale("en") do + I18n.with_locale(:en) do + iab = PublicBody.internal_admin_body + iab.should_not be_nil + end + end + with_default_locale("es") do + I18n.with_locale(:es) do + iab = PublicBody.internal_admin_body + iab.should_not be_nil + end + end + end + +end + + describe PublicBody, " when dealing public body locales" do it "shouldn't fail if it internal_admin_body was created in a locale other than the default" do # first time, do it with the non-default locale - PublicBody.with_locale(:es) do + I18n.with_locale(:es) do PublicBody.internal_admin_body end @@ -252,22 +355,24 @@ describe PublicBody, " when loading CSV files" do errors.should == [] notes.size.should == 2 notes[0].should == "line 1: creating new authority 'aBody' (locale: en):\n\t{\"name\":\"aBody\"}" - notes[1].should =~ /Notes: Some bodies are in database, but not in CSV file:\n( [A-Za-z ]+\n)*You may want to delete them manually.\n/ + notes[1].should =~ /Notes: Some bodies are in database, but not in CSV file:\n( .+\n)*You may want to delete them manually.\n/ end it "should do a dry run successfully" do original_count = PublicBody.count - csv_contents = load_file_fixture("fake-authority-type.csv") + csv_contents = normalize_string_to_utf8(load_file_fixture("fake-authority-type.csv")) errors, notes = PublicBody.import_csv(csv_contents, '', 'replace', true, 'someadmin') # true means dry run errors.should == [] - notes.size.should == 4 - notes[0..2].should == [ + notes.size.should == 6 + notes[0..4].should == [ "line 1: creating new authority 'North West Fake Authority' (locale: en):\n\t\{\"name\":\"North West Fake Authority\",\"request_email\":\"north_west_foi@localhost\"\}", "line 2: creating new authority 'Scottish Fake Authority' (locale: en):\n\t\{\"name\":\"Scottish Fake Authority\",\"request_email\":\"scottish_foi@localhost\"\}", "line 3: creating new authority 'Fake Authority of Northern Ireland' (locale: en):\n\t\{\"name\":\"Fake Authority of Northern Ireland\",\"request_email\":\"ni_foi@localhost\"\}", + "line 4: creating new authority 'Gobierno de Aragón' (locale: en):\n\t\{\"name\":\"Gobierno de Arag\\u00f3n\",\"request_email\":\"spain_foi@localhost\"}", + "line 5: creating new authority 'Nordic æøå' (locale: en):\n\t{\"name\":\"Nordic \\u00e6\\u00f8\\u00e5\",\"request_email\":\"no_foi@localhost\"}" ] - notes[3].should =~ /Notes: Some bodies are in database, but not in CSV file:\n( [A-Za-z ]+\n)*You may want to delete them manually.\n/ + notes[5].should =~ /Notes: Some bodies are in database, but not in CSV file:\n( .+\n)*You may want to delete them manually.\n/ PublicBody.count.should == original_count end @@ -275,34 +380,38 @@ describe PublicBody, " when loading CSV files" do it "should do full run successfully" do original_count = PublicBody.count - csv_contents = load_file_fixture("fake-authority-type.csv") + csv_contents = normalize_string_to_utf8(load_file_fixture("fake-authority-type.csv")) errors, notes = PublicBody.import_csv(csv_contents, '', 'replace', false, 'someadmin') # false means real run errors.should == [] - notes.size.should == 4 - notes[0..2].should == [ + notes.size.should == 6 + notes[0..4].should == [ "line 1: creating new authority 'North West Fake Authority' (locale: en):\n\t\{\"name\":\"North West Fake Authority\",\"request_email\":\"north_west_foi@localhost\"\}", "line 2: creating new authority 'Scottish Fake Authority' (locale: en):\n\t\{\"name\":\"Scottish Fake Authority\",\"request_email\":\"scottish_foi@localhost\"\}", "line 3: creating new authority 'Fake Authority of Northern Ireland' (locale: en):\n\t\{\"name\":\"Fake Authority of Northern Ireland\",\"request_email\":\"ni_foi@localhost\"\}", + "line 4: creating new authority 'Gobierno de Aragón' (locale: en):\n\t\{\"name\":\"Gobierno de Arag\\u00f3n\",\"request_email\":\"spain_foi@localhost\"}", + "line 5: creating new authority 'Nordic æøå' (locale: en):\n\t{\"name\":\"Nordic \\u00e6\\u00f8\\u00e5\",\"request_email\":\"no_foi@localhost\"}" ] - notes[3].should =~ /Notes: Some bodies are in database, but not in CSV file:\n( [A-Za-z ]+\n)*You may want to delete them manually.\n/ + notes[5].should =~ /Notes: Some bodies are in database, but not in CSV file:\n( .+\n)*You may want to delete them manually.\n/ - PublicBody.count.should == original_count + 3 + PublicBody.count.should == original_count + 5 end it "should do imports without a tag successfully" do original_count = PublicBody.count - csv_contents = load_file_fixture("fake-authority-type.csv") + csv_contents = normalize_string_to_utf8(load_file_fixture("fake-authority-type.csv")) errors, notes = PublicBody.import_csv(csv_contents, '', 'replace', false, 'someadmin') # false means real run errors.should == [] - notes.size.should == 4 - notes[0..2].should == [ + notes.size.should == 6 + notes[0..4].should == [ "line 1: creating new authority 'North West Fake Authority' (locale: en):\n\t\{\"name\":\"North West Fake Authority\",\"request_email\":\"north_west_foi@localhost\"\}", "line 2: creating new authority 'Scottish Fake Authority' (locale: en):\n\t\{\"name\":\"Scottish Fake Authority\",\"request_email\":\"scottish_foi@localhost\"\}", "line 3: creating new authority 'Fake Authority of Northern Ireland' (locale: en):\n\t\{\"name\":\"Fake Authority of Northern Ireland\",\"request_email\":\"ni_foi@localhost\"\}", + "line 4: creating new authority 'Gobierno de Aragón' (locale: en):\n\t\{\"name\":\"Gobierno de Arag\\u00f3n\",\"request_email\":\"spain_foi@localhost\"}", + "line 5: creating new authority 'Nordic æøå' (locale: en):\n\t{\"name\":\"Nordic \\u00e6\\u00f8\\u00e5\",\"request_email\":\"no_foi@localhost\"}" ] - notes[3].should =~ /Notes: Some bodies are in database, but not in CSV file:\n( [A-Za-z ]+\n)*You may want to delete them manually.\n/ - PublicBody.count.should == original_count + 3 + notes[5].should =~ /Notes: Some bodies are in database, but not in CSV file:\n( .+\n)*You may want to delete them manually.\n/ + PublicBody.count.should == original_count + 5 end it "should handle a field list and fields out of order" do @@ -317,7 +426,7 @@ describe PublicBody, " when loading CSV files" do "line 3: creating new authority 'Scottish Fake Authority' (locale: en):\n\t\{\"name\":\"Scottish Fake Authority\",\"request_email\":\"scottish_foi@localhost\",\"home_page\":\"http://scottish.org\",\"tag_string\":\"scottish\"\}", "line 4: creating new authority 'Fake Authority of Northern Ireland' (locale: en):\n\t\{\"name\":\"Fake Authority of Northern Ireland\",\"request_email\":\"ni_foi@localhost\",\"tag_string\":\"fake aTag\"\}", ] - notes[3].should =~ /Notes: Some bodies are in database, but not in CSV file:\n( [A-Za-z ]+\n)*You may want to delete them manually.\n/ + notes[3].should =~ /Notes: Some bodies are in database, but not in CSV file:\n( .+\n)*You may want to delete them manually.\n/ PublicBody.count.should == original_count end @@ -374,11 +483,11 @@ describe PublicBody, " when loading CSV files" do "line 4: creating new authority 'Fake Authority of Northern Ireland' (locale: en):\n\t{\"name\":\"Fake Authority of Northern Ireland\",\"request_email\":\"ni_foi@localhost\",\"tag_string\":\"fake aTag\"}", "line 4: creating new authority 'Fake Authority of Northern Ireland' (locale: es):\n\t{\"name\":\"Autoridad Irlandesa\"}", ] - notes[6].should =~ /Notes: Some bodies are in database, but not in CSV file:\n( [A-Za-z ]+\n)*You may want to delete them manually.\n/ + notes[6].should =~ /Notes: Some bodies are in database, but not in CSV file:\n( .+\n)*You may want to delete them manually.\n/ PublicBody.count.should == original_count + 3 - # XXX Not sure why trying to do a PublicBody.with_locale fails here. Seems related to + # XXX Not sure why trying to do a I18n.with_locale fails here. Seems related to # the way categories are loaded every time from the PublicBody class. For now we just # test some translation was done. body = PublicBody.find_by_name('North West Fake Authority') @@ -400,10 +509,24 @@ describe PublicBody, " when loading CSV files" do "line 3: creating new authority 'Scottish Fake Authority' (locale: en):\n\t{\"name\":\"Scottish Fake Authority\",\"request_email\":\"scottish_foi@localhost\",\"home_page\":\"http://scottish.org\",\"tag_string\":\"scottish\"}", "line 4: creating new authority 'Fake Authority of Northern Ireland' (locale: en):\n\t{\"name\":\"Fake Authority of Northern Ireland\",\"request_email\":\"ni_foi@localhost\",\"tag_string\":\"fake aTag\"}", ] - notes[3].should =~ /Notes: Some bodies are in database, but not in CSV file:\n( [A-Za-z ]+\n)*You may want to delete them manually.\n/ + notes[3].should =~ /Notes: Some bodies are in database, but not in CSV file:\n( .+\n)*You may want to delete them manually.\n/ PublicBody.count.should == original_count end + + it "should be able to load CSV from a file as well as a string" do + # Essentially the same code is used for import_csv_from_file + # as import_csv, so this is just a basic check that + # import_csv_from_file can load from a file at all. (It would + # be easy to introduce a regression that broke this, because + # of the confusing change in behaviour of CSV.parse between + # Ruby 1.8 and 1.9.) + original_count = PublicBody.count + filename = file_fixture_name('fake-authority-type-with-field-names.csv') + PublicBody.import_csv_from_file(filename, '', 'replace', false, 'someadmin') + PublicBody.count.should == original_count + 3 + end + end describe PublicBody do @@ -456,8 +579,91 @@ end describe PublicBody, " when override all public body request emails set" do it "should return the overridden request email" do - MySociety::Config.should_receive(:get).with("OVERRIDE_ALL_PUBLIC_BODY_REQUEST_EMAILS", "").twice.and_return("catch_all_test_email@foo.com") + AlaveteliConfiguration.should_receive(:override_all_public_body_request_emails).twice.and_return("catch_all_test_email@foo.com") @geraldine = public_bodies(:geraldine_public_body) @geraldine.request_email.should == "catch_all_test_email@foo.com" end end + +describe PublicBody, "when calculating statistics" do + + it "should not include unclassified or hidden requests in percentages" do + with_hidden_and_successful_requests do + totals_data = PublicBody.get_request_totals(n=3, + highest=true, + minimum_requests=1) + # For the total number of requests, we still include + # hidden or unclassified requests: + totals_data['public_bodies'][-1].name.should == "Geraldine Quango" + totals_data['totals'][-1].should == 4 + + # However, for percentages, don't include the hidden or + # unclassified requests. So, for the Geraldine Quango + # we've made sure that there are only two visible and + # classified requests, one of which is successful, so the + # percentage should be 50%: + + percentages_data = PublicBody.get_request_percentages(column='info_requests_successful_count', + n=3, + highest=false, + minimum_requests=1) + geraldine_index = percentages_data['public_bodies'].index do |pb| + pb.name == "Geraldine Quango" + end + + percentages_data['y_values'][geraldine_index].should == 50 + end + end + + it "should only return totals for those with at least a minimum number of requests" do + minimum_requests = 1 + with_enough_info_requests = PublicBody.where(["info_requests_count >= ?", + minimum_requests]).length + all_data = PublicBody.get_request_totals 4, true, minimum_requests + all_data['public_bodies'].length.should == with_enough_info_requests + end + + it "should only return percentages for those with at least a minimum number of requests" do + with_hidden_and_successful_requests do + # With minimum requests at 3, this should return nil + # (corresponding to zero public bodies) since the only + # public body with just more than 3 info requests (The + # Geraldine Quango) has a hidden and an unclassified + # request within this block: + minimum_requests = 3 + with_enough_info_requests = PublicBody.where(["info_requests_visible_classified_count >= ?", + minimum_requests]).length + all_data = PublicBody.get_request_percentages(column='info_requests_successful_count', + n=10, + true, + minimum_requests) + all_data.should be_nil + end + end + + it "should only return those with at least a minimum number of requests, but not tagged 'test'" do + hpb = PublicBody.find_by_name 'Department for Humpadinking' + + original_tag_string = hpb.tag_string + hpb.add_tag_if_not_already_present 'test' + + begin + minimum_requests = 1 + with_enough_info_requests = PublicBody.where(["info_requests_count >= ?", minimum_requests]) + all_data = PublicBody.get_request_totals 4, true, minimum_requests + all_data['public_bodies'].length.should == 3 + ensure + hpb.tag_string = original_tag_string + end + end + +end + +describe PublicBody, 'when asked for popular bodies' do + + it 'should return bodies correctly when passed the hyphenated version of the locale' do + AlaveteliConfiguration.stub!(:frontpage_publicbody_examples).and_return('') + PublicBody.popular_bodies('he-IL').should == [public_bodies(:humpadink_public_body)] + end + +end diff --git a/spec/models/purge_request_spec.rb b/spec/models/purge_request_spec.rb index 94fe01317..02b3d685d 100644 --- a/spec/models/purge_request_spec.rb +++ b/spec/models/purge_request_spec.rb @@ -1,12 +1,24 @@ +# == Schema Information +# +# Table name: purge_requests +# +# id :integer not null, primary key +# url :string(255) +# created_at :datetime not null +# model :string(255) not null +# model_id :integer not null +# + require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') require 'fakeweb' -describe PurgeRequest, "purging things" do +describe PurgeRequest, "purging things" do before do + PurgeRequest.destroy_all FakeWeb.last_request = nil end - it 'should issue purge requests to the server' do + it 'should issue purge requests to the server' do req = PurgeRequest.new(:url => "/begone_from_here", :model => "don't care", :model_id => "don't care") @@ -16,7 +28,7 @@ describe PurgeRequest, "purging things" do PurgeRequest.all().count.should == 0 end - it 'should fail silently for a misconfigured server' do + it 'should fail silently for a misconfigured server' do FakeWeb.register_uri(:get, %r|brokenv|, :body => "BROKEN") config = MySociety::Config.load_default() config['VARNISH_HOST'] = "brokencache" @@ -29,4 +41,4 @@ describe PurgeRequest, "purging things" do PurgeRequest.all().count.should == 0 end end - + diff --git a/spec/models/raw_email_spec.rb b/spec/models/raw_email_spec.rb index ff2830a62..f86b35e99 100644 --- a/spec/models/raw_email_spec.rb +++ b/spec/models/raw_email_spec.rb @@ -1,3 +1,10 @@ +# == Schema Information +# +# Table name: raw_emails +# +# id :integer not null, primary key +# + require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') describe User, "manipulating a raw email" do diff --git a/spec/models/request_mailer_spec.rb b/spec/models/request_mailer_spec.rb deleted file mode 100644 index 20d401a63..000000000 --- a/spec/models/request_mailer_spec.rb +++ /dev/null @@ -1,380 +0,0 @@ -require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') - -describe RequestMailer, " when receiving incoming mail" do - before(:each) do - load_raw_emails_data - ActionMailer::Base.deliveries = [] - end - - it "should append it to the appropriate request" do - ir = info_requests(:fancy_dog_request) - ir.incoming_messages.size.should == 1 # in the fixture - receive_incoming_mail('incoming-request-plain.email', ir.incoming_email) - ir.incoming_messages.size.should == 2 # one more arrives - ir.info_request_events[-1].incoming_message_id.should_not be_nil - - deliveries = ActionMailer::Base.deliveries - deliveries.size.should == 1 - mail = deliveries[0] - mail.to.should == [ 'bob@localhost' ] # to the user who sent fancy_dog_request - deliveries.clear - end - - it "should store mail in holding pen and send to admin when the email is not to any information request" do - ir = info_requests(:fancy_dog_request) - ir.incoming_messages.size.should == 1 - InfoRequest.holding_pen_request.incoming_messages.size.should == 0 - receive_incoming_mail('incoming-request-plain.email', 'dummy@localhost') - ir.incoming_messages.size.should == 1 - InfoRequest.holding_pen_request.incoming_messages.size.should == 1 - last_event = InfoRequest.holding_pen_request.incoming_messages[0].info_request.get_last_event - last_event.params[:rejected_reason].should == "Could not identify the request from the email address" - - deliveries = ActionMailer::Base.deliveries - deliveries.size.should == 1 - mail = deliveries[0] - mail.to.should == [ Configuration::contact_email ] - deliveries.clear - end - - it "should store mail in holding pen and send to admin when the from email is empty and only authorites can reply" do - ir = info_requests(:fancy_dog_request) - ir.allow_new_responses_from = 'authority_only' - ir.handle_rejected_responses = 'holding_pen' - ir.save! - ir.incoming_messages.size.should == 1 - InfoRequest.holding_pen_request.incoming_messages.size.should == 0 - receive_incoming_mail('incoming-request-plain.email', ir.incoming_email, "") - ir.incoming_messages.size.should == 1 - InfoRequest.holding_pen_request.incoming_messages.size.should == 1 - last_event = InfoRequest.holding_pen_request.incoming_messages[0].info_request.get_last_event - last_event.params[:rejected_reason].should =~ /there is no "From" address/ - - deliveries = ActionMailer::Base.deliveries - deliveries.size.should == 1 - mail = deliveries[0] - mail.to.should == [ Configuration::contact_email ] - deliveries.clear - end - - it "should store mail in holding pen and send to admin when the from email is unknown and only authorites can reply" do - ir = info_requests(:fancy_dog_request) - ir.allow_new_responses_from = 'authority_only' - ir.handle_rejected_responses = 'holding_pen' - ir.save! - ir.incoming_messages.size.should == 1 - InfoRequest.holding_pen_request.incoming_messages.size.should == 0 - receive_incoming_mail('incoming-request-plain.email', ir.incoming_email, "frob@nowhere.com") - ir.incoming_messages.size.should == 1 - InfoRequest.holding_pen_request.incoming_messages.size.should == 1 - last_event = InfoRequest.holding_pen_request.incoming_messages[0].info_request.get_last_event - last_event.params[:rejected_reason].should =~ /Only the authority can reply/ - - deliveries = ActionMailer::Base.deliveries - deliveries.size.should == 1 - mail = deliveries[0] - mail.to.should == [ Configuration::contact_email ] - deliveries.clear - end - - it "should return incoming mail to sender when a request is stopped fully for spam" do - # mark request as anti-spam - ir = info_requests(:fancy_dog_request) - ir.allow_new_responses_from = 'nobody' - ir.handle_rejected_responses = 'bounce' - ir.save! - - # test what happens if something arrives - ir.incoming_messages.size.should == 1 # in the fixture - receive_incoming_mail('incoming-request-plain.email', ir.incoming_email) - ir.incoming_messages.size.should == 1 # nothing should arrive - - # should be a message back to sender - deliveries = ActionMailer::Base.deliveries - deliveries.size.should == 1 - mail = deliveries[0] - mail.to.should == [ 'geraldinequango@localhost' ] - # check attached bounce is good copy of incoming-request-plain.email - mail.multipart?.should == true - mail.parts.size.should == 2 - message_part = mail.parts[0].to_s - bounced_mail = MailHandler.mail_from_raw_email(mail.parts[1].body) - bounced_mail.to.should == [ ir.incoming_email ] - bounced_mail.from.should == [ 'geraldinequango@localhost' ] - bounced_mail.body.include?("That's so totally a rubbish question").should be_true - message_part.include?("marked to no longer receive responses").should be_true - deliveries.clear - end - - it "should return incoming mail to sender if not authority when a request is stopped for non-authority spam" do - # mark request as anti-spam - ir = info_requests(:fancy_dog_request) - ir.allow_new_responses_from = 'authority_only' - ir.handle_rejected_responses = 'bounce' - ir.save! - - # Test what happens if something arrives from authority domain (@localhost) - ir.incoming_messages.size.should == 1 # in the fixture - receive_incoming_mail('incoming-request-plain.email', ir.incoming_email, "Geraldine <geraldinequango@localhost>") - ir.incoming_messages.size.should == 2 # one more arrives - - # ... should get "responses arrived" message for original requester - deliveries = ActionMailer::Base.deliveries - deliveries.size.should == 1 - mail = deliveries[0] - mail.to.should == [ 'bob@localhost' ] # to the user who sent fancy_dog_request - deliveries.clear - - # Test what happens if something arrives from another domain - ir.incoming_messages.size.should == 2 # in fixture and above - receive_incoming_mail('incoming-request-plain.email', ir.incoming_email, "dummy-address@dummy.localhost") - ir.incoming_messages.size.should == 2 # nothing should arrive - - # ... should be a bounce message back to sender - deliveries = ActionMailer::Base.deliveries - deliveries.size.should == 1 - mail = deliveries[0] - mail.to.should == [ 'dummy-address@dummy.localhost' ] - deliveries.clear - end - - it "should send all new responses to holding pen if a request is marked to do so" do - # mark request as anti-spam - ir = info_requests(:fancy_dog_request) - ir.allow_new_responses_from = 'nobody' - ir.handle_rejected_responses = 'holding_pen' - ir.save! - - # test what happens if something arrives - ir = info_requests(:fancy_dog_request) - ir.incoming_messages.size.should == 1 - InfoRequest.holding_pen_request.incoming_messages.size.should == 0 - receive_incoming_mail('incoming-request-plain.email', ir.incoming_email) - ir.incoming_messages.size.should == 1 - InfoRequest.holding_pen_request.incoming_messages.size.should == 1 # arrives in holding pen - last_event = InfoRequest.holding_pen_request.incoming_messages[0].info_request.get_last_event - last_event.params[:rejected_reason].should =~ /allow new responses from nobody/ - - # should be a message to admin regarding holding pen - deliveries = ActionMailer::Base.deliveries - deliveries.size.should == 1 - mail = deliveries[0] - mail.to.should == [ Configuration::contact_email ] - deliveries.clear - end - - it "should destroy the messages sent to a request if marked to do so" do - ActionMailer::Base.deliveries.clear - # mark request as anti-spam - ir = info_requests(:fancy_dog_request) - ir.allow_new_responses_from = 'nobody' - ir.handle_rejected_responses = 'blackhole' - ir.save! - - # test what happens if something arrives - should be nothing - ir = info_requests(:fancy_dog_request) - ir.incoming_messages.size.should == 1 - InfoRequest.holding_pen_request.incoming_messages.size.should == 0 - receive_incoming_mail('incoming-request-plain.email', ir.incoming_email) - ir.incoming_messages.size.should == 1 - InfoRequest.holding_pen_request.incoming_messages.size.should == 0 - - # should be no messages to anyone - deliveries = ActionMailer::Base.deliveries - deliveries.size.should == 0 - end - - - it "should not mutilate long URLs when trying to word wrap them" do - long_url = 'http://www.this.is.quite.a.long.url.flourish.org/there.is.no.way.it.is.short.whatsoever' - body = "This is a message with quite a long URL in it. It also has a paragraph, being this one that has quite a lot of text in it to. Enough to test the wrapping of itself. - -#{long_url} - -And a paragraph afterwards." - wrapped = MySociety::Format.wrap_email_body_by_paragraphs(body) - wrapped.should include(long_url) - end -end - - -describe RequestMailer, "when sending reminders to requesters to classify a response to their request" do - - before do - Time.stub!(:now).and_return(Time.utc(2007, 11, 12, 23, 59)) - @mock_event = mock_model(InfoRequestEvent) - @mock_response = mock_model(IncomingMessage) - @mock_user = mock_model(User) - @mock_request = mock_model(InfoRequest, :get_last_response_event_id => @mock_event.id, - :get_last_response => @mock_response, - :user_id => 2, - :url_title => 'test_title', - :user => @mock_user) - InfoRequest.stub!(:find).and_return([@mock_request]) - RequestMailer.stub!(:deliver_new_response_reminder_alert) - @sent_alert = mock_model(UserInfoRequestSentAlert, :user= =>nil, - :info_request= => nil, - :alert_type= => nil, - :info_request_event_id= => nil, - :save! => true) - UserInfoRequestSentAlert.stub!(:new).and_return(@sent_alert) - end - - def send_alerts - RequestMailer.alert_new_response_reminders_internal(7, 'new_response_reminder_1') - end - - it 'should ask for all requests that are awaiting description and whose latest response is older - than the number of days given and that are not the holding pen' do - expected_conditions = [ "awaiting_description = ? - AND (SELECT created_at - FROM info_request_events - WHERE info_request_events.info_request_id = info_requests.id - AND info_request_events.event_type = 'response' - ORDER BY created_at desc LIMIT 1) < ? - AND url_title != 'holding_pen' - AND user_id IS NOT NULL".split(' ').join(' '), - true, Time.now() - 7.days ] - - # compare the query string ignoring any spacing differences - InfoRequest.should_receive(:find) do |all, query_params| - query_string = query_params[:conditions][0] - query_params[:conditions][0] = query_string.split(' ').join(' ') - query_params[:conditions].should == expected_conditions - query_params[:include].should == [ :user ] - query_params[:order].should == 'info_requests.id' - end - - send_alerts - end - - it 'should raise an error if a request does not have a last response event id' do - @mock_request.stub!(:get_last_response_event_id).and_return(nil) - expected_message = "internal error, no last response while making alert new response reminder, request id #{@mock_request.id}" - lambda{ send_alerts }.should raise_error(expected_message) - end - - it 'should check to see if an alert matching the attributes of the one to be sent has already been sent' do - expected_params = {:conditions => [ "alert_type = ? and user_id = ? and info_request_id = ? and info_request_event_id = ?", - 'new_response_reminder_1', 2, @mock_request.id, @mock_event.id]} - UserInfoRequestSentAlert.should_receive(:find).with(:first, expected_params) - send_alerts - end - - describe 'if an alert matching the attributes of the reminder to be sent has already been sent' do - - before do - UserInfoRequestSentAlert.stub!(:find).and_return(mock_model(UserInfoRequestSentAlert)) - end - - it 'should not send the reminder' do - RequestMailer.should_not_receive(:deliver_new_response_reminder_alert) - send_alerts - end - - end - - describe 'if no alert matching the attributes of the reminder to be sent has already been sent' do - - before do - UserInfoRequestSentAlert.stub!(:find).and_return(nil) - end - - it 'should store the information that the reminder has been sent' do - mock_sent_alert = mock_model(UserInfoRequestSentAlert) - UserInfoRequestSentAlert.stub!(:new).and_return(mock_sent_alert) - mock_sent_alert.should_receive(:info_request=).with(@mock_request) - mock_sent_alert.should_receive(:user=).with(@mock_user) - mock_sent_alert.should_receive(:alert_type=).with('new_response_reminder_1') - mock_sent_alert.should_receive(:info_request_event_id=).with(@mock_request.get_last_response_event_id) - mock_sent_alert.should_receive(:save!) - send_alerts - end - - it 'should send the reminder' do - RequestMailer.should_receive(:deliver_new_response_reminder_alert) - send_alerts - end - end - -end - -describe RequestMailer, 'when sending mail when someone has updated an old unclassified request' do - - before do - @user = mock_model(User, :name_and_email => 'test name and email') - @public_body = mock_model(PublicBody, :name => 'Test public body') - @info_request = mock_model(InfoRequest, :user => @user, - :law_used_full => 'Freedom of Information', - :title => 'Test request', - :public_body => @public_body, - :display_status => 'Refused.', - :url_title => 'test_request') - @mail = RequestMailer.create_old_unclassified_updated(@info_request) - end - - it 'should have the subject "Someone has updated the status of your request"' do - @mail.subject.should == 'Someone has updated the status of your request' - end - - it 'should tell them what status was picked' do - @mail.body.should match(/"refused."/) - end - - it 'should contain the request path' do - @mail.body.should match(/request\/test_request/) - end - -end - - -describe RequestMailer, 'when sending a new response email' do - - before do - @user = mock_model(User, :name_and_email => 'test name and email') - @public_body = mock_model(PublicBody, :name => 'Test public body') - @info_request = mock_model(InfoRequest, :user => @user, - :law_used_full => 'Freedom of Information', - :title => 'Here is a character that needs quoting …', - :public_body => @public_body, - :display_status => 'Refused.', - :url_title => 'test_request') - @incoming_message = mock_model(IncomingMessage, :info_request => @info_request) - end - - it 'should not error when sending mails requests with characters requiring quoting in the subject' do - @mail = RequestMailer.create_new_response(@info_request, @incoming_message) - end - -end - -describe RequestMailer, 'requires_admin' do - before(:each) do - user = mock_model(User, :name_and_email => 'Bruce Jones', - :name => 'Bruce Jones') - @info_request = mock_model(InfoRequest, :user => user, - :described_state => 'error_message', - :title => 'Test request', - :url_title => 'test_request', - :law_used_short => 'FOI', - :id => 123) - end - - it 'body should contain the full admin URL' do - mail = RequestMailer.deliver_requires_admin(@info_request) - - mail.body.should include('http://test.host/en/admin/request/show/123') - end - - context 'has an ADMIN_BASE_URL set' do - before(:each) do - Configuration::should_receive(:admin_base_url).and_return('http://our.proxy.server/admin/alaveteli/') - end - - it 'body should contain the full admin URL' do - mail = RequestMailer.deliver_requires_admin(@info_request) - - mail.body.should include('http://our.proxy.server/admin/alaveteli/request/show/123') - end - end -end diff --git a/spec/models/track_mailer_spec.rb b/spec/models/track_mailer_spec.rb deleted file mode 100644 index 9bf03c3d0..000000000 --- a/spec/models/track_mailer_spec.rb +++ /dev/null @@ -1,198 +0,0 @@ -require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') - -describe TrackMailer do - - describe 'when sending email alerts for tracked things' do - - before do - TrackMailer.stub!(:deliver_event_digest) - Time.stub!(:now).and_return(Time.utc(2007, 11, 12, 23, 59)) - end - - it 'should ask for all the users whose last daily track email was sent more than a day ago' do - expected_conditions = [ "last_daily_track_email < ?", Time.utc(2007, 11, 11, 23, 59)] - User.should_receive(:find).with(:all, :conditions => expected_conditions).and_return([]) - TrackMailer.alert_tracks - end - - describe 'for each user' do - - before do - @user = mock_model(User, :no_xapian_reindex= => false, - :last_daily_track_email= => true, - :save! => true, - :url_name => 'test-name', - :get_locale => 'en', - :should_be_emailed? => true) - User.stub!(:find).and_return([@user]) - @user.stub!(:receive_email_alerts).and_return(true) - @user.stub!(:no_xapian_reindex=) - end - - it 'should ask for any daily track things for the user' do - expected_conditions = [ "tracking_user_id = ? and track_medium = ?", @user.id, 'email_daily' ] - TrackThing.should_receive(:find).with(:all, :conditions => expected_conditions).and_return([]) - TrackMailer.alert_tracks - end - - - it 'should set the no_xapian_reindex flag on the user' do - @user.should_receive(:no_xapian_reindex=).with(true) - TrackMailer.alert_tracks - end - - it 'should update the time of the user\'s last daily tracking email' do - @user.should_receive(:last_daily_track_email=).with(Time.now) - @user.should_receive(:save!) - TrackMailer.alert_tracks - end - it 'should return true' do - TrackMailer.alert_tracks.should == true - end - - - describe 'for each tracked thing' do - - before do - @track_things_sent_emails_array = [] - @track_things_sent_emails_array.stub!(:find).and_return([]) # this is for the date range find (created in last 14 days) - @track_thing = mock_model(TrackThing, :track_query => 'test query', - :track_things_sent_emails => @track_things_sent_emails_array, - :created_at => Time.utc(2007, 11, 9, 23, 59)) - TrackThing.stub!(:find).and_return([@track_thing]) - @track_things_sent_email = mock_model(TrackThingsSentEmail, :save! => true, - :track_thing_id= => true, - :info_request_event_id= => true) - TrackThingsSentEmail.stub!(:new).and_return(@track_things_sent_email) - @xapian_search = mock('xapian search', :results => []) - @found_event = mock_model(InfoRequestEvent, :described_at => @track_thing.created_at + 1.day) - @search_result = {:model => @found_event} - InfoRequest.stub!(:full_search).and_return(@xapian_search) - end - - it 'should ask for the events returned by the tracking query' do - InfoRequest.should_receive(:full_search).with([InfoRequestEvent], 'test query', 'described_at', true, nil, 100, 1).and_return(@xapian_search) - TrackMailer.alert_tracks - end - - it 'should not include in the email any events that the user has already been sent a tracking email about' do - sent_email = mock_model(TrackThingsSentEmail, :info_request_event_id => @found_event.id) - @track_things_sent_emails_array.stub!(:find).and_return([sent_email]) # this is for the date range find (created in last 14 days) - @xapian_search.stub!(:results).and_return([@search_result]) - TrackMailer.should_not_receive(:deliver_event_digest) - TrackMailer.alert_tracks - end - - it 'should not include in the email any events not sent in a previous tracking email that were described before the track was set up' do - @found_event.stub!(:described_at).and_return(@track_thing.created_at - 1.day) - @xapian_search.stub!(:results).and_return([@search_result]) - TrackMailer.should_not_receive(:deliver_event_digest) - TrackMailer.alert_tracks - end - - it 'should include in the email any events that the user has not been sent a tracking email on that have been described since the track was set up' do - @found_event.stub!(:described_at).and_return(@track_thing.created_at + 1.day) - @xapian_search.stub!(:results).and_return([@search_result]) - TrackMailer.should_receive(:deliver_event_digest) - TrackMailer.alert_tracks - end - - it 'should raise an error if a non-event class is returned by the tracking query' do - @xapian_search.stub!(:results).and_return([{:model => 'string class'}]) - lambda{ TrackMailer.alert_tracks }.should raise_error('need to add other types to TrackMailer.alert_tracks (unalerted)') - end - - it 'should record that a tracking email has been sent for each event that has been included in the email' do - @xapian_search.stub!(:results).and_return([@search_result]) - sent_email = mock_model(TrackThingsSentEmail) - TrackThingsSentEmail.should_receive(:new).and_return(sent_email) - sent_email.should_receive(:track_thing_id=).with(@track_thing.id) - sent_email.should_receive(:info_request_event_id=).with(@found_event.id) - sent_email.should_receive(:save!) - TrackMailer.alert_tracks - end - end - - end - - describe 'when a user should not be emailed' do - before do - @user = mock_model(User, :no_xapian_reindex= => false, - :last_daily_track_email= => true, - :save! => true, - :url_name => 'test-name', - :should_be_emailed? => false) - User.stub!(:find).and_return([@user]) - @user.stub!(:receive_email_alerts).and_return(true) - @user.stub!(:no_xapian_reindex=) - end - - it 'should not ask for any daily track things for the user' do - expected_conditions = [ "tracking_user_id = ? and track_medium = ?", @user.id, 'email_daily' ] - TrackThing.should_not_receive(:find).with(:all, :conditions => expected_conditions).and_return([]) - TrackMailer.alert_tracks - end - - it 'should not ask for any daily track things for the user if they have receive_email_alerts off but could otherwise be emailed' do - @user.stub(:should_be_emailed?).and_return(true) - @user.stub(:receive_email_alerts).and_return(false) - expected_conditions = [ "tracking_user_id = ? and track_medium = ?", @user.id, 'email_daily' ] - TrackThing.should_not_receive(:find).with(:all, :conditions => expected_conditions).and_return([]) - TrackMailer.alert_tracks - end - - it 'should not set the no_xapian_reindex flag on the user' do - @user.should_not_receive(:no_xapian_reindex=).with(true) - TrackMailer.alert_tracks - end - - it 'should not update the time of the user\'s last daily tracking email' do - @user.should_not_receive(:last_daily_track_email=).with(Time.now) - @user.should_not_receive(:save!) - TrackMailer.alert_tracks - end - it 'should return false' do - TrackMailer.alert_tracks.should == false - end - end - - end - - describe 'delivering the email' do - - before do - @post_redirect = mock_model(PostRedirect, :save! => true, - :email_token => "token") - PostRedirect.stub!(:new).and_return(@post_redirect) - ActionMailer::Base.deliveries = [] - end - - it 'should deliver one email, with right headers' do - @user = mock_model(User, - :name_and_email => MailHandler.address_from_name_and_email('Tippy Test', 'tippy@localhost'), - :url_name => 'tippy_test' - ) - - TrackMailer.deliver_event_digest(@user, []) # no items in it email for minimal test - deliveries = ActionMailer::Base.deliveries - if deliveries.size > 1 # debugging if there is an error - deliveries.each do |d| - $stderr.puts "------------------------------" - $stderr.puts d.body - $stderr.puts "------------------------------" - end - end - deliveries.size.should == 1 - mail = deliveries[0] - - mail['Auto-Submitted'].to_s.should == 'auto-generated' - mail['Precedence'].to_s.should == 'bulk' - - deliveries.clear - end - end - -end - - - diff --git a/spec/models/track_thing_spec.rb b/spec/models/track_thing_spec.rb index c42eb5e8b..1c582564b 100644 --- a/spec/models/track_thing_spec.rb +++ b/spec/models/track_thing_spec.rb @@ -1,3 +1,19 @@ +# == Schema Information +# +# Table name: track_things +# +# id :integer not null, primary key +# tracking_user_id :integer not null +# track_query :string(255) not null +# info_request_id :integer +# tracked_user_id :integer +# public_body_id :integer +# track_medium :string(255) not null +# track_type :string(255) default("internal_error"), not null +# created_at :datetime +# updated_at :datetime +# + require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') describe TrackThing, "when tracking changes" do @@ -23,7 +39,7 @@ describe TrackThing, "when tracking changes" do it "will find existing tracks which are the same" do track_thing = TrackThing.create_track_for_search_query('fancy dog') - found_track = TrackThing.find_by_existing_track(users(:silly_name_user), track_thing) + found_track = TrackThing.find_existing(users(:silly_name_user), track_thing) found_track.should == @track_thing end diff --git a/spec/models/track_things_sent_email_spec.rb b/spec/models/track_things_sent_email_spec.rb index 6166f42ab..4675d0847 100644 --- a/spec/models/track_things_sent_email_spec.rb +++ b/spec/models/track_things_sent_email_spec.rb @@ -1,3 +1,16 @@ +# == Schema Information +# +# Table name: track_things_sent_emails +# +# id :integer not null, primary key +# track_thing_id :integer not null +# info_request_event_id :integer +# user_id :integer +# public_body_id :integer +# created_at :datetime +# updated_at :datetime +# + require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') describe TrackThingsSentEmail, "when tracking things sent email" do diff --git a/spec/models/user_info_request_sent_alert_spec.rb b/spec/models/user_info_request_sent_alert_spec.rb index 971c5b8c1..69be1092b 100644 --- a/spec/models/user_info_request_sent_alert_spec.rb +++ b/spec/models/user_info_request_sent_alert_spec.rb @@ -1,3 +1,14 @@ +# == Schema Information +# +# Table name: user_info_request_sent_alerts +# +# id :integer not null, primary key +# user_id :integer not null +# info_request_id :integer not null +# alert_type :string(255) not null +# info_request_event_id :integer +# + require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') describe UserInfoRequestSentAlert, " when blah" do diff --git a/spec/models/user_mailer_spec.rb b/spec/models/user_mailer_spec.rb deleted file mode 100644 index 0792aaab2..000000000 --- a/spec/models/user_mailer_spec.rb +++ /dev/null @@ -1,8 +0,0 @@ -require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') - -describe UserMailer, " when blah" do - before do - end -end - - diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index e31c3f1b5..b6f48dad3 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1,3 +1,27 @@ +# == Schema Information +# +# Table name: users +# +# id :integer not null, primary key +# email :string(255) not null +# name :string(255) not null +# hashed_password :string(255) not null +# salt :string(255) not null +# created_at :datetime not null +# updated_at :datetime not null +# email_confirmed :boolean default(FALSE), not null +# url_name :text not null +# last_daily_track_email :datetime default(2000-01-01 00:00:00 UTC) +# admin_level :string(255) default("none"), not null +# ban_text :text default(""), not null +# about_me :text default(""), not null +# locale :string(255) +# email_bounced_at :datetime +# email_bounce_message :text default(""), not null +# no_limit :boolean default(FALSE), not null +# receive_email_alerts :boolean default(TRUE), not null +# + require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') describe User, "making up the URL name" do @@ -27,11 +51,22 @@ describe User, "showing the name" do @user.name.should == 'Some Name' end - it 'should show if user has been banned' do - @user.ban_text = "Naughty user" - @user.name.should == 'Some Name (Account suspended)' + describe 'if user has been banned' do + + before do + @user.ban_text = "Naughty user" + end + + it 'should show an "Account suspended" suffix' do + @user.name.should == 'Some Name (Account suspended)' + end + + it 'should return a string when the user has been banned, not a SafeBuffer' do + @user.name.class.should == String + end end + end describe User, " when authenticating" do @@ -152,10 +187,10 @@ end describe User, "when reindexing referencing models" do before do - @request_event = safe_mock_model(InfoRequestEvent, :xapian_mark_needs_index => true) - @request = safe_mock_model(InfoRequest, :info_request_events => [@request_event]) - @comment_event = safe_mock_model(InfoRequestEvent, :xapian_mark_needs_index => true) - @comment = safe_mock_model(Comment, :info_request_events => [@comment_event]) + @request_event = mock_model(InfoRequestEvent, :xapian_mark_needs_index => true) + @request = mock_model(InfoRequest, :info_request_events => [@request_event]) + @comment_event = mock_model(InfoRequestEvent, :xapian_mark_needs_index => true) + @comment = mock_model(Comment, :info_request_events => [@comment_event]) @user = User.new(:comments => [@comment], :info_requests => [@request]) end @@ -302,3 +337,34 @@ describe User, "when emails have bounced" do user.email_bounce_message.should == "The reason we think the email bounced (e.g. a bounce message)" end end + +describe User, "when calculating if a user has exceeded the request limit" do + + before do + @info_request = FactoryGirl.create(:info_request) + @user = @info_request.user + end + + it 'should return false if no request limit is set' do + AlaveteliConfiguration.stub!(:max_requests_per_user_per_day).and_return nil + @user.exceeded_limit?.should be_false + end + + it 'should return false if the user has not submitted more than the limit' do + AlaveteliConfiguration.stub!(:max_requests_per_user_per_day).and_return(2) + @user.exceeded_limit?.should be_false + end + + it 'should return true if the user has submitted more than the limit' do + AlaveteliConfiguration.stub!(:max_requests_per_user_per_day).and_return(0) + @user.exceeded_limit?.should be_true + end + + it 'should return false if the user is allowed to make batch requests' do + @user.can_make_batch_requests = true + AlaveteliConfiguration.stub!(:max_requests_per_user_per_day).and_return(0) + @user.exceeded_limit?.should be_false + end + + +end diff --git a/spec/models/xapian_spec.rb b/spec/models/xapian_spec.rb index 8c99d550f..a1e060d8e 100644 --- a/spec/models/xapian_spec.rb +++ b/spec/models/xapian_spec.rb @@ -1,3 +1,4 @@ +# encoding: utf-8 require File.expand_path(File.dirname(__FILE__) + '/../spec_helper') describe User, " when indexing users with Xapian" do @@ -8,8 +9,7 @@ describe User, " when indexing users with Xapian" do end it "should search by name" do - # def InfoRequest.full_search(models, query, order, ascending, collapse, per_page, page) - xapian_object = InfoRequest.full_search([User], "Silly", 'created_at', true, nil, 100, 1) + xapian_object = ActsAsXapian::Search.new([User], "Silly", :limit => 100) xapian_object.results.size.should == 1 xapian_object.results[0][:model].should == users(:silly_name_user) end @@ -17,8 +17,7 @@ describe User, " when indexing users with Xapian" do it "should search by 'about me' text" do user = users(:bob_smith_user) - # def InfoRequest.full_search(models, query, order, ascending, collapse, per_page, page) - xapian_object = InfoRequest.full_search([User], "stuff", 'created_at', true, nil, 100, 1) + xapian_object = ActsAsXapian::Search.new([User], "stuff", :limit => 100) xapian_object.results.size.should == 1 xapian_object.results[0][:model].should == user @@ -26,10 +25,10 @@ describe User, " when indexing users with Xapian" do user.save! update_xapian_index - xapian_object = InfoRequest.full_search([User], "stuff", 'created_at', true, nil, 100, 1) + xapian_object = ActsAsXapian::Search.new([User], "stuff", :limit => 100) xapian_object.results.size.should == 0 - xapian_object = InfoRequest.full_search([User], "aardvark", 'created_at', true, nil, 100, 1) + xapian_object = ActsAsXapian::Search.new([User], "aardvark", :limit => 100) xapian_object.results.size.should == 1 xapian_object.results[0][:model].should == user end @@ -42,26 +41,26 @@ describe PublicBody, " when indexing public bodies with Xapian" do end it "should search index the main name field" do - xapian_object = InfoRequest.full_search([PublicBody], "humpadinking", 'created_at', true, nil, 100, 1) + xapian_object = ActsAsXapian::Search.new([PublicBody], "humpadinking", :limit => 100) xapian_object.results.size.should == 1 xapian_object.results[0][:model].should == public_bodies(:humpadink_public_body) end it "should search index the notes field" do - xapian_object = InfoRequest.full_search([PublicBody], "albatross", 'created_at', true, nil, 100, 1) + xapian_object = ActsAsXapian::Search.new([PublicBody], "albatross", :limit => 100) xapian_object.results.size.should == 1 xapian_object.results[0][:model].should == public_bodies(:humpadink_public_body) end it "should delete public bodies from the index when they are destroyed" do - xapian_object = InfoRequest.full_search([PublicBody], "albatross", 'created_at', true, nil, 100, 1) + xapian_object = ActsAsXapian::Search.new([PublicBody], "albatross", :limit => 100) xapian_object.results.size.should == 1 xapian_object.results[0][:model].should == public_bodies(:humpadink_public_body) public_bodies(:forlorn_public_body).destroy update_xapian_index - xapian_object = InfoRequest.full_search([PublicBody], "lonely", 'created_at', true, nil, 100, 1) + xapian_object = ActsAsXapian::Search.new([PublicBody], "lonely", :limit => 100) xapian_object.results.should == [] end @@ -75,13 +74,13 @@ describe PublicBody, " when indexing requests by body they are to" do end it "should find requests to the body" do - xapian_object = InfoRequest.full_search([InfoRequestEvent], "requested_from:tgq", 'created_at', true, nil, 100, 1) + xapian_object = ActsAsXapian::Search.new([InfoRequestEvent], "requested_from:tgq", :limit => 100) xapian_object.results.size.should == PublicBody.find_by_url_name("tgq").info_requests.map(&:info_request_events).flatten.size end it "should update index correctly when URL name of body changes" do # initial search - xapian_object = InfoRequest.full_search([InfoRequestEvent], "requested_from:tgq", 'created_at', true, nil, 100, 1) + xapian_object = ActsAsXapian::Search.new([InfoRequestEvent], "requested_from:tgq", :limit => 100) xapian_object.results.size.should == PublicBody.find_by_url_name("tgq").info_requests.map(&:info_request_events).flatten.size models_found_before = xapian_object.results.map { |x| x[:model] } @@ -93,9 +92,9 @@ describe PublicBody, " when indexing requests by body they are to" do update_xapian_index # check we get results expected - xapian_object = InfoRequest.full_search([InfoRequestEvent], "requested_from:tgq", 'created_at', true, nil, 100, 1) + xapian_object = ActsAsXapian::Search.new([InfoRequestEvent], "requested_from:tgq", :limit => 100) xapian_object.results.size.should == 0 - xapian_object = InfoRequest.full_search([InfoRequestEvent], "requested_from:gq", 'created_at', true, nil, 100, 1) + xapian_object = ActsAsXapian::Search.new([InfoRequestEvent], "requested_from:gq", :limit => 100) xapian_object.results.size.should == PublicBody.find_by_url_name("gq").info_requests.map(&:info_request_events).flatten.size models_found_after = xapian_object.results.map { |x| x[:model] } @@ -113,11 +112,11 @@ describe PublicBody, " when indexing requests by body they are to" do update_xapian_index # check we get results expected - xapian_object = InfoRequest.full_search([InfoRequestEvent], "requested_from:tgq", 'created_at', true, nil, 100, 1) + xapian_object = ActsAsXapian::Search.new([InfoRequestEvent], "requested_from:tgq", :limit => 100) xapian_object.results.size.should == 0 - xapian_object = InfoRequest.full_search([InfoRequestEvent], "requested_from:gq", 'created_at', true, nil, 100, 1) + xapian_object = ActsAsXapian::Search.new([InfoRequestEvent], "requested_from:gq", :limit => 100) xapian_object.results.size.should == 0 - xapian_object = InfoRequest.full_search([InfoRequestEvent], "requested_from:" + body.url_name, 'created_at', true, nil, 100, 1) + xapian_object = ActsAsXapian::Search.new([InfoRequestEvent], "requested_from:#{body.url_name}", :limit => 100) xapian_object.results.size.should == public_bodies(:geraldine_public_body).info_requests.map(&:info_request_events).flatten.size models_found_after = xapian_object.results.map { |x| x[:model] } end @@ -130,13 +129,14 @@ describe User, " when indexing requests by user they are from" do end it "should find requests from the user" do - xapian_object = InfoRequest.full_search([InfoRequestEvent], "requested_by:bob_smith", 'created_at', true, nil, 100, 1) + xapian_object = ActsAsXapian::Search.new([InfoRequestEvent], "requested_by:bob_smith", + :sort_by_prefix => 'created_at', :sort_by_ascending => true, :limit => 100) xapian_object.results.map{|x|x[:model]}.should =~ InfoRequestEvent.all(:conditions => "info_request_id in (select id from info_requests where user_id = #{users(:bob_smith_user).id})") end it "should find just the sent message events from a particular user" do - # def InfoRequest.full_search(models, query, order, ascending, collapse, per_page, page) - xapian_object = InfoRequest.full_search([InfoRequestEvent], "requested_by:bob_smith variety:sent", 'created_at', true, nil, 100, 1) + xapian_object = ActsAsXapian::Search.new([InfoRequestEvent], "requested_by:bob_smith variety:sent", + :sort_by_prefix => 'created_at', :sort_by_ascending => true, :limit => 100) xapian_object.results.map{|x|x[:model]}.should =~ InfoRequestEvent.all(:conditions => "info_request_id in (select id from info_requests where user_id = #{users(:bob_smith_user).id}) and event_type = 'sent'") xapian_object.results[2][:model].should == info_request_events(:useless_outgoing_message_event) xapian_object.results[1][:model].should == info_request_events(:silly_outgoing_message_event) @@ -150,8 +150,9 @@ describe User, " when indexing requests by user they are from" do update_xapian_index - # def InfoRequest.full_search(models, query, order, ascending, collapse, per_page, page) - xapian_object = InfoRequest.full_search([InfoRequestEvent], "requested_by:bob_smith", 'created_at', true, 'request_collapse', 100, 1) + xapian_object = ActsAsXapian::Search.new([InfoRequestEvent], "requested_by:bob_smith", + :sort_by_prefix => 'created_at', :sort_by_ascending => true, + :collapse_by_prefix => 'request_collapse', :limit => 100) xapian_object.results.map{|x|x[:model].info_request}.should =~ InfoRequest.all(:conditions => "user_id = #{users(:bob_smith_user).id}") end @@ -172,8 +173,7 @@ describe User, " when indexing requests by user they are from" do update_xapian_index - # def InfoRequest.full_search(models, query, order, ascending, collapse, per_page, page) - xapian_object = InfoRequest.full_search([InfoRequestEvent], "requested_by:john_k", 'created_at', true, 'request_collapse', 100, 1) + xapian_object = ActsAsXapian::Search.new([InfoRequestEvent], "requested_by:john_k", :limit => 100) xapian_object.results.size.should == 1 xapian_object.results[0][:model].should == info_request_events(:silly_outgoing_message_event) end @@ -181,7 +181,8 @@ describe User, " when indexing requests by user they are from" do it "should update index correctly when URL name of user changes" do # initial search - xapian_object = InfoRequest.full_search([InfoRequestEvent], "requested_by:bob_smith", 'created_at', true, nil, 100, 1) + xapian_object = ActsAsXapian::Search.new([InfoRequestEvent], "requested_by:bob_smith", + :sort_by_prefix => 'created_at', :sort_by_ascending => true, :limit => 100) xapian_object.results.map{|x|x[:model]}.should =~ InfoRequestEvent.all(:conditions => "info_request_id in (select id from info_requests where user_id = #{users(:bob_smith_user).id})") models_found_before = xapian_object.results.map { |x| x[:model] } @@ -193,9 +194,10 @@ describe User, " when indexing requests by user they are from" do update_xapian_index # check we get results expected - xapian_object = InfoRequest.full_search([InfoRequestEvent], "requested_by:bob_smith", 'created_at', true, nil, 100, 1) + xapian_object = ActsAsXapian::Search.new([InfoRequestEvent], "requested_by:bob_smith", :limit => 100) xapian_object.results.size.should == 0 - xapian_object = InfoRequest.full_search([InfoRequestEvent], "requested_by:robert_smith", 'created_at', true, nil, 100, 1) + xapian_object = ActsAsXapian::Search.new([InfoRequestEvent], "requested_by:robert_smith", + :sort_by_prefix => 'created_at', :sort_by_ascending => true, :limit => 100) models_found_after = xapian_object.results.map { |x| x[:model] } models_found_before.should == models_found_after end @@ -208,13 +210,13 @@ describe User, " when indexing comments by user they are by" do end it "should find requests from the user" do - xapian_object = InfoRequest.full_search([InfoRequestEvent], "commented_by:silly_emnameem", 'created_at', true, nil, 100, 1) + xapian_object = ActsAsXapian::Search.new([InfoRequestEvent], "commented_by:silly_emnameem", :limit => 100) xapian_object.results.size.should == 1 end it "should update index correctly when URL name of user changes" do # initial search - xapian_object = InfoRequest.full_search([InfoRequestEvent], "commented_by:silly_emnameem", 'created_at', true, nil, 100, 1) + xapian_object = ActsAsXapian::Search.new([InfoRequestEvent], "commented_by:silly_emnameem", :limit => 100) xapian_object.results.size.should == 1 models_found_before = xapian_object.results.map { |x| x[:model] } @@ -226,9 +228,9 @@ describe User, " when indexing comments by user they are by" do update_xapian_index # check we get results expected - xapian_object = InfoRequest.full_search([InfoRequestEvent], "commented_by:silly_emnameem", 'created_at', true, nil, 100, 1) + xapian_object = ActsAsXapian::Search.new([InfoRequestEvent], "commented_by:silly_emnameem", :limit => 100) xapian_object.results.size.should == 0 - xapian_object = InfoRequest.full_search([InfoRequestEvent], "commented_by:silly_name", 'created_at', true, nil, 100, 1) + xapian_object = ActsAsXapian::Search.new([InfoRequestEvent], "commented_by:silly_name", :limit => 100) xapian_object.results.size.should == 1 models_found_after = xapian_object.results.map { |x| x[:model] } @@ -243,7 +245,7 @@ describe InfoRequest, " when indexing requests by their title" do end it "should find events for the request" do - xapian_object = InfoRequest.full_search([InfoRequestEvent], "request:how_much_public_money_is_wasted_o", 'created_at', true, nil, 100, 1) + xapian_object = ActsAsXapian::Search.new([InfoRequestEvent], "request:how_much_public_money_is_wasted_o", :limit => 100) xapian_object.results.size.should == 1 xapian_object.results[0][:model] == info_request_events(:silly_outgoing_message_event) end @@ -257,9 +259,9 @@ describe InfoRequest, " when indexing requests by their title" do update_xapian_index # check we get results expected - xapian_object = InfoRequest.full_search([InfoRequestEvent], "request:how_much_public_money_is_wasted_o", 'created_at', true, nil, 100, 1) + xapian_object = ActsAsXapian::Search.new([InfoRequestEvent], "request:how_much_public_money_is_wasted_o", :limit => 100) xapian_object.results.size.should == 0 - xapian_object = InfoRequest.full_search([InfoRequestEvent], "request:really_naughty", 'created_at', true, nil, 100, 1) + xapian_object = ActsAsXapian::Search.new([InfoRequestEvent], "request:really_naughty", :limit => 100) xapian_object.results.size.should == 1 xapian_object.results[0][:model] == info_request_events(:silly_outgoing_message_event) end @@ -277,11 +279,11 @@ describe InfoRequest, " when indexing requests by tag" do ir.save! update_xapian_index - xapian_object = InfoRequest.full_search([InfoRequestEvent], "tag:bunnyrabbit", 'created_at', true, nil, 100, 1) + xapian_object = ActsAsXapian::Search.new([InfoRequestEvent], "tag:bunnyrabbit", :limit => 100) xapian_object.results.size.should == 1 xapian_object.results[0][:model] == info_request_events(:silly_outgoing_message_event) - xapian_object = InfoRequest.full_search([InfoRequestEvent], "tag:orangeaardvark", 'created_at', true, nil, 100, 1) + xapian_object = ActsAsXapian::Search.new([InfoRequestEvent], "tag:orangeaardvark", :limit => 100) xapian_object.results.size.should == 0 end end @@ -298,14 +300,14 @@ describe PublicBody, " when indexing authorities by tag" do body.save! update_xapian_index - xapian_object = InfoRequest.full_search([PublicBody], "tag:mice", 'created_at', true, nil, 100, 1) + xapian_object = ActsAsXapian::Search.new([PublicBody], "tag:mice", :limit => 100) xapian_object.results.size.should == 1 xapian_object.results[0][:model] == public_bodies(:geraldine_public_body) - xapian_object = InfoRequest.full_search([PublicBody], "tag:mice:3", 'created_at', true, nil, 100, 1) + xapian_object = ActsAsXapian::Search.new([PublicBody], "tag:mice:3", :limit => 100) xapian_object.results.size.should == 1 xapian_object.results[0][:model] == public_bodies(:geraldine_public_body) - xapian_object = InfoRequest.full_search([PublicBody], "tag:orangeaardvark", 'created_at', true, nil, 100, 1) + xapian_object = ActsAsXapian::Search.new([PublicBody], "tag:orangeaardvark", :limit => 100) xapian_object.results.size.should == 0 end end @@ -327,11 +329,11 @@ describe PublicBody, " when only indexing selected things on a rebuild" do values = false texts = false rebuild_xapian_index(terms, values, texts, dropfirst) - xapian_object = InfoRequest.full_search([PublicBody], "tag:mice", 'created_at', true, nil, 100, 1) + xapian_object = ActsAsXapian::Search.new([PublicBody], "tag:mice", :limit => 100) xapian_object.results.size.should == 0 - xapian_object = InfoRequest.full_search([PublicBody], "frobzn", 'created_at', true, nil, 100, 1) + xapian_object = ActsAsXapian::Search.new([PublicBody], "frobzn", :limit => 100) xapian_object.results.size.should == 0 - xapian_object = InfoRequest.full_search([PublicBody], "variety:authority", 'created_at', true, nil, 100, 1) + xapian_object = ActsAsXapian::Search.new([PublicBody], "variety:authority", :limit => 100) xapian_object.results.map{|x|x[:model]}.should =~ PublicBody.all # only reindex 'tag' and text dropfirst = true @@ -339,32 +341,78 @@ describe PublicBody, " when only indexing selected things on a rebuild" do values = false texts = true rebuild_xapian_index(terms, values, texts, dropfirst) - xapian_object = InfoRequest.full_search([PublicBody], "tag:mice", 'created_at', true, nil, 100, 1) + xapian_object = ActsAsXapian::Search.new([PublicBody], "tag:mice", :limit => 100) xapian_object.results.size.should == 1 - xapian_object = InfoRequest.full_search([PublicBody], "frobzn", 'created_at', true, nil, 100, 1) + xapian_object = ActsAsXapian::Search.new([PublicBody], "frobzn", :limit => 100) xapian_object.results.size.should == 1 - xapian_object = InfoRequest.full_search([PublicBody], "variety:authority", 'created_at', true, nil, 100, 1) + xapian_object = ActsAsXapian::Search.new([PublicBody], "variety:authority", :limit => 100) xapian_object.results.size.should == 0 # only reindex 'variety' term, but keeping the existing data in-place dropfirst = false terms = "V" texts = false rebuild_xapian_index(terms, values, texts, dropfirst) - xapian_object = InfoRequest.full_search([PublicBody], "tag:mice", 'created_at', true, nil, 100, 1) + xapian_object = ActsAsXapian::Search.new([PublicBody], "tag:mice", :limit => 100) xapian_object.results.size.should == 1 - xapian_object = InfoRequest.full_search([PublicBody], "frobzn", 'created_at', true, nil, 100, 1) + xapian_object = ActsAsXapian::Search.new([PublicBody], "frobzn", :limit => 100) xapian_object.results.size.should == 1 - xapian_object = InfoRequest.full_search([PublicBody], "variety:authority", 'created_at', true, nil, 100, 1) + xapian_object = ActsAsXapian::Search.new([PublicBody], "variety:authority", :limit => 100) xapian_object.results.map{|x|x[:model]}.should =~ PublicBody.all # only reindex 'variety' term, blowing away existing data dropfirst = true rebuild_xapian_index(terms, values, texts, dropfirst) - xapian_object = InfoRequest.full_search([PublicBody], "tag:mice", 'created_at', true, nil, 100, 1) + xapian_object = ActsAsXapian::Search.new([PublicBody], "tag:mice", :limit => 100) xapian_object.results.size.should == 0 - xapian_object = InfoRequest.full_search([PublicBody], "frobzn", 'created_at', true, nil, 100, 1) + xapian_object = ActsAsXapian::Search.new([PublicBody], "frobzn", :limit => 100) xapian_object.results.size.should == 0 - xapian_object = InfoRequest.full_search([PublicBody], "variety:authority", 'created_at', true, nil, 100, 1) + xapian_object = ActsAsXapian::Search.new([PublicBody], "variety:authority", :limit => 100) xapian_object.results.map{|x|x[:model]}.should =~ PublicBody.all end end +# I would expect ActsAsXapian to have some tests under lib/acts_as_xapian, but +# it looks like this is not the case. Putting a test here instead. +describe ActsAsXapian::Search, "#words_to_highlight" do + before(:each) do + load_raw_emails_data + get_fixtures_xapian_index + end + + it "should return a list of words used in the search" do + s = ActsAsXapian::Search.new([PublicBody], "albatross words", :limit => 100) + s.words_to_highlight.should == ["albatross", "words"] + end + + it "should remove any operators" do + s = ActsAsXapian::Search.new([PublicBody], "albatross words tag:mice", :limit => 100) + s.words_to_highlight.should == ["albatross", "words"] + end + + # This is the current behaviour but it seems a little simplistic to me + it "should separate punctuation" do + s = ActsAsXapian::Search.new([PublicBody], "The doctor's patient", :limit => 100) + s.words_to_highlight.should == ["The", "doctor", "s", "patient"] + end + + it "should handle non-ascii characters" do + s = ActsAsXapian::Search.new([PublicBody], "adatigénylés words tag:mice", :limit => 100) + s.words_to_highlight.should == ["adatigénylés", "words"] + end + +end + +describe InfoRequestEvent, " when faced with a race condition during xapian_mark_needs_index" do + + before(:each) do + load_raw_emails_data + get_fixtures_xapian_index + end + + it 'should not raise an error but should fail silently' do + with_duplicate_xapian_job_creation do + ir = info_requests(:naughty_chicken_request) + ir.reindex_request_events + end + end + +end |