mirror of
https://github.com/opf/openproject.git
synced 2026-06-13 19:20:00 +00:00
Add more specs
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module IncomingEmails
|
||||
class UnauthorizedAction < StandardError; end
|
||||
class MissingInformation < StandardError; end
|
||||
end
|
||||
@@ -28,10 +28,6 @@
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
module IncomingEmails
|
||||
class UnauthorizedAction < StandardError; end
|
||||
|
||||
class MissingInformation < StandardError; end
|
||||
|
||||
class DispatchService
|
||||
include ActionView::Helpers::SanitizeHelper
|
||||
|
||||
@@ -54,6 +50,10 @@ module IncomingEmails
|
||||
handlers.unshift(handler_class)
|
||||
end
|
||||
|
||||
def self.remove_handler(handler_class)
|
||||
handlers.delete(handler_class)
|
||||
end
|
||||
|
||||
attr_reader :email, :sender_email, :user, :options, :success, :logs
|
||||
|
||||
def initialize(email, options:)
|
||||
@@ -94,6 +94,7 @@ module IncomingEmails
|
||||
# the subject. Additionally, the subject structure might change, e.g. via localization changes.
|
||||
def dispatch
|
||||
call = call_matching_handler
|
||||
return if call.nil?
|
||||
|
||||
@success = call.success?
|
||||
log_handler_call(call)
|
||||
@@ -101,13 +102,10 @@ module IncomingEmails
|
||||
call.result
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
log "could not save record: #{e.message}", :error
|
||||
nil
|
||||
rescue MissingInformation => e
|
||||
log "missing information from #{user}: #{e.message}", :error
|
||||
nil
|
||||
rescue UnauthorizedAction
|
||||
log "unauthorized attempt from #{user}", :error
|
||||
nil
|
||||
end
|
||||
|
||||
def log_handler_call(call)
|
||||
@@ -127,7 +125,7 @@ module IncomingEmails
|
||||
:info,
|
||||
report: false
|
||||
|
||||
false
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
@@ -262,10 +260,10 @@ module IncomingEmails
|
||||
|
||||
message = "MailHandler: #{message}"
|
||||
Rails.logger.public_send(level, message)
|
||||
nil
|
||||
end
|
||||
|
||||
def assign_options(value)
|
||||
# rubocop:disable Metrics/AbcSize
|
||||
def assign_options(value) # rubocop:disable Metrics/AbcSize
|
||||
options = value.dup
|
||||
|
||||
options[:issue] ||= {}
|
||||
@@ -280,6 +278,7 @@ module IncomingEmails
|
||||
options[:allow_override] << :type unless options[:issue].has_key?(:type)
|
||||
# Priority overridable by default
|
||||
options[:allow_override] << :priority unless options[:issue].has_key?(:priority)
|
||||
|
||||
options[:no_permission_check] = ActiveRecord::Type::Boolean.new.cast(options[:no_permission_check])
|
||||
|
||||
options
|
||||
|
||||
@@ -49,6 +49,10 @@ module IncomingEmails::Handlers
|
||||
raise NotImplementedError, "Subclasses must implement handle method"
|
||||
end
|
||||
|
||||
def cleaned_up_text_body
|
||||
cleanup_body(plain_text_body)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# The receive_* methods have been moved to specific handler classes:
|
||||
@@ -188,7 +192,7 @@ module IncomingEmails::Handlers
|
||||
# * parse the email To field
|
||||
# * specific project (eg. Setting.mail_handler_target_project)
|
||||
target = Project.find_by(identifier: get_keyword(:project))
|
||||
raise MissingInformation.new("Unable to determine target project") if target.nil?
|
||||
raise IncomingEmails::MissingInformation.new("Unable to determine target project") if target.nil?
|
||||
|
||||
target
|
||||
end
|
||||
@@ -209,10 +213,6 @@ module IncomingEmails::Handlers
|
||||
end
|
||||
end
|
||||
|
||||
def cleaned_up_text_body
|
||||
cleanup_body(plain_text_body)
|
||||
end
|
||||
|
||||
# Removes the email body of text after the truncation configurations.
|
||||
def cleanup_body(body)
|
||||
delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).compact_blank.map { |s| Regexp.escape(s) }
|
||||
|
||||
@@ -42,7 +42,7 @@ module IncomingEmails::Handlers
|
||||
private
|
||||
|
||||
# Receives a reply to a forum message
|
||||
def receive_message_reply(message_id) # rubocop:disable Metrics/AbcSize
|
||||
def receive_message_reply(message_id)
|
||||
message = Message.find_by(id: message_id)
|
||||
if message
|
||||
message = message.root
|
||||
@@ -55,17 +55,22 @@ module IncomingEmails::Handlers
|
||||
ServiceResult.failure(message: "ignoring reply from [#{sender_email}] to a locked topic",
|
||||
mesage_type: :warn)
|
||||
else
|
||||
reply = Message.new(subject: email.subject.gsub(%r{^.*msg\d+\]}, "").strip,
|
||||
content: cleaned_up_text_body)
|
||||
reply.author = user
|
||||
reply.forum = message.forum
|
||||
message.children << reply
|
||||
add_attachments(reply)
|
||||
reply
|
||||
ServiceResult.success(result: reply,
|
||||
message: "Reply added to message ##{message.id}")
|
||||
create_reply_message(message)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def create_reply_message(root)
|
||||
reply = Message.new(subject: email.subject.gsub(%r{^.*msg\d+\]}, "").strip,
|
||||
content: cleaned_up_text_body)
|
||||
reply.author = user
|
||||
reply.forum = root.forum
|
||||
root.children << reply
|
||||
add_attachments(reply)
|
||||
reply
|
||||
|
||||
ServiceResult.success(result: reply,
|
||||
message: "Reply added to message ##{root.id}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -73,17 +73,18 @@ module IncomingEmails::Handlers
|
||||
|
||||
call = create_work_package(project)
|
||||
|
||||
call.message = if call.success?
|
||||
"work_package created by #{user}"
|
||||
else
|
||||
"work_package could not be created by #{user} due to ##{call.errors.full_messages}"
|
||||
end
|
||||
call.message =
|
||||
if call.success?
|
||||
"work_package created by #{user}"
|
||||
else
|
||||
"work_package could not be created by #{user} due to ##{call.errors.full_messages}"
|
||||
end
|
||||
|
||||
call
|
||||
end
|
||||
|
||||
# Adds a note to an existing work package
|
||||
def receive_work_package_reply(work_package_id)
|
||||
def receive_work_package_reply(work_package_id) # rubocop:disable Metrics/AbcSize
|
||||
work_package = ::WorkPackage.find_by(id: work_package_id)
|
||||
return unless work_package
|
||||
|
||||
|
||||
@@ -33,7 +33,8 @@ module IncomingEmails
|
||||
##
|
||||
# Code copied from base class and extended with optional options parameter
|
||||
# as well as force_encoding support.
|
||||
def self.receive(raw_mail, options = {})
|
||||
def self.receive(input, options = {})
|
||||
raw_mail = input.dup
|
||||
raw_mail.force_encoding("ASCII-8BIT") if raw_mail.respond_to?(:force_encoding)
|
||||
|
||||
ActiveSupport::Notifications.instrument("receive.action_mailer") do |payload|
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
|
||||
+3
-1
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
@@ -31,7 +33,7 @@ require "net/pop"
|
||||
module Redmine
|
||||
module POP3
|
||||
class << self
|
||||
def check(pop_options = {}, options = {})
|
||||
def check(pop_options = {}, options = {}) # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
|
||||
host = pop_options[:host] || "127.0.0.1"
|
||||
port = pop_options[:port] || "110"
|
||||
apop = (pop_options[:apop].to_s == "1")
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe IncomingEmails::MailHandler do
|
||||
RSpec.describe IncomingEmails::MailHandler do # rubocop:disable RSpec/SpecFilePathFormat
|
||||
# we need these run first so the anonymous and system users are created and
|
||||
# there is a default work package priority to save any work packages
|
||||
shared_let(:anno_user) { User.anonymous }
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe IncomingEmails::DispatchService do
|
||||
let(:email) { instance_double(Mail::Message) }
|
||||
let(:text_part) { instance_double(Mail::Part) }
|
||||
let(:body) { instance_double(Mail::Body) }
|
||||
let(:options) { {} }
|
||||
let(:service) { described_class.new(email, options:) }
|
||||
|
||||
before do
|
||||
allow(body).to receive_messages(decoded: "Test body".dup)
|
||||
allow(text_part).to receive_messages(body:, charset: "UTF-8")
|
||||
allow(email).to receive_messages(text_part: text_part, from: ["test@example.com"])
|
||||
end
|
||||
|
||||
describe "#initialize" do
|
||||
it "initializes with email and options" do
|
||||
expect(service.email).to eq(email)
|
||||
expect(service.options).to include(options)
|
||||
end
|
||||
|
||||
it "extracts sender email from the email" do
|
||||
expect(service.sender_email).to eq("test@example.com")
|
||||
end
|
||||
|
||||
it "initializes empty logs array" do
|
||||
expect(service.logs).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
describe ".handlers" do
|
||||
it "returns the default handlers" do
|
||||
expect(described_class.handlers).to include(
|
||||
IncomingEmails::Handlers::MessageReply,
|
||||
IncomingEmails::Handlers::WorkPackage
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe ".register_handler" do
|
||||
let(:custom_handler) { class_double(IncomingEmails::Handlers::WorkPackage) }
|
||||
|
||||
after do
|
||||
described_class.remove_handler(custom_handler)
|
||||
end
|
||||
|
||||
it "adds handler to the beginning of the handlers list" do
|
||||
described_class.register_handler(custom_handler)
|
||||
expect(described_class.handlers).to include(custom_handler)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#call!" do
|
||||
let(:user) { build_stubbed(:user) }
|
||||
|
||||
before do
|
||||
allow(service).to receive(:determine_actor)
|
||||
allow(service).to receive_messages(ignore_mail?: false, user: user)
|
||||
allow(service).to receive(:dispatch)
|
||||
end
|
||||
|
||||
it "calls determine_actor" do
|
||||
service.call!
|
||||
expect(service).to have_received(:determine_actor)
|
||||
end
|
||||
|
||||
it "calls dispatch when user is present" do
|
||||
service.call!
|
||||
expect(service).to have_received(:dispatch)
|
||||
end
|
||||
|
||||
it "does not call dispatch when user is not present" do
|
||||
allow(service).to receive(:user).and_return(nil)
|
||||
service.call!
|
||||
expect(service).not_to have_received(:dispatch)
|
||||
end
|
||||
|
||||
it "returns early when mail should be ignored" do
|
||||
allow(service).to receive(:ignore_mail?).and_return(true)
|
||||
service.call!
|
||||
expect(service).not_to have_received(:determine_actor)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#ignore_mail?" do
|
||||
it "returns false by default" do
|
||||
allow(service).to receive_messages(mail_from_system?: false, ignored_by_header?: false, ignored_user?: false)
|
||||
|
||||
expect(service.send(:ignore_mail?)).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
describe "#mail_from_system?" do
|
||||
context "when email is from system address" do
|
||||
before do
|
||||
allow(Setting).to receive(:mail_from).and_return("system@example.com")
|
||||
allow(email).to receive(:from).and_return(["system@example.com"])
|
||||
end
|
||||
|
||||
it "returns true" do
|
||||
service = described_class.new(email, options:)
|
||||
expect(service.send(:mail_from_system?)).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context "when email is not from system address" do
|
||||
before do
|
||||
allow(Setting).to receive(:mail_from).and_return("system@example.com")
|
||||
allow(email).to receive(:from).and_return(["user@example.com"])
|
||||
end
|
||||
|
||||
it "returns false" do
|
||||
service = described_class.new(email, options:)
|
||||
expect(service.send(:mail_from_system?)).to be_falsey
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#ignored_by_header?" do
|
||||
context "with auto-response suppress header" do
|
||||
before do
|
||||
allow(email).to receive(:header).and_return({ "X-Auto-Response-Suppress" => "OOF" })
|
||||
end
|
||||
|
||||
it "returns true" do
|
||||
expect(service.send(:ignored_by_header?)).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context "with auto-submitted header" do
|
||||
before do
|
||||
allow(email).to receive(:header).and_return({ "Auto-Submitted" => "auto-replied" })
|
||||
end
|
||||
|
||||
it "returns true" do
|
||||
expect(service.send(:ignored_by_header?)).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context "without ignored headers" do
|
||||
before do
|
||||
allow(email).to receive(:header).and_return({})
|
||||
end
|
||||
|
||||
it "returns false" do
|
||||
expect(service.send(:ignored_by_header?)).to be_falsey
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#object_reference_from_header" do
|
||||
context "with valid reference header" do
|
||||
before do
|
||||
allow(email).to receive(:references).and_return(["<op.work_packages-123@example.com>"])
|
||||
end
|
||||
|
||||
it "extracts object reference" do
|
||||
reference = service.send(:object_reference_from_header)
|
||||
expect(reference).to eq({ klass: "work_packages", id: 123 })
|
||||
end
|
||||
end
|
||||
|
||||
context "without valid reference header" do
|
||||
before do
|
||||
allow(email).to receive(:references).and_return([])
|
||||
end
|
||||
|
||||
it "returns empty hash" do
|
||||
reference = service.send(:object_reference_from_header)
|
||||
expect(reference).to eq({})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#instantiate_matching_handler" do
|
||||
let(:handler_class) { class_double(IncomingEmails::Handlers::WorkPackage) }
|
||||
let(:handler_instance) { instance_double(IncomingEmails::Handlers::WorkPackage) }
|
||||
|
||||
before do
|
||||
allow(service).to receive(:object_reference_from_header).and_return({})
|
||||
allow(described_class).to receive(:handlers).and_return([handler_class])
|
||||
allow(handler_class).to receive_messages(handles?: true, new: handler_instance)
|
||||
end
|
||||
|
||||
it "finds and instantiates matching handler" do
|
||||
result = service.send(:instantiate_matching_handler)
|
||||
expect(result).to eq(handler_instance)
|
||||
end
|
||||
|
||||
it "returns nil when no handler matches" do
|
||||
allow(handler_class).to receive(:handles?).and_return(false)
|
||||
result = service.send(:instantiate_matching_handler)
|
||||
expect(result).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -55,5 +55,104 @@ RSpec.describe IncomingEmails::Handlers::Base do
|
||||
expect(subject.cleaned_up_text_body).to eq("Subject:foo\nDescription:bar")
|
||||
end
|
||||
end
|
||||
|
||||
context "with string delimiters" do
|
||||
before do
|
||||
allow(Setting).to receive_messages(mail_handler_body_delimiters: "---", mail_handler_body_delimiter_regex: "")
|
||||
end
|
||||
|
||||
it "removes content after delimiter" do
|
||||
body = "Before delimiter\n---\nAfter delimiter"
|
||||
result = subject.send(:cleanup_body, body)
|
||||
expect(result).to eq("Before delimiter")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#get_keyword" do
|
||||
let(:plain_text_body) { "Project: test\nStatus: Open\nSome content here".dup }
|
||||
let(:options) { { allow_override: [:project], issue: {} } }
|
||||
|
||||
it "extracts keyword from body" do
|
||||
result = subject.send(:get_keyword, :project)
|
||||
expect(result).to eq("test")
|
||||
end
|
||||
|
||||
it "returns nil for non-existent keyword" do
|
||||
result = subject.send(:get_keyword, :nonexistent)
|
||||
expect(result).to be_nil
|
||||
end
|
||||
|
||||
it "respects allow_override option" do
|
||||
result = subject.send(:get_keyword, :status)
|
||||
expect(result).to be_nil # status not in allow_override
|
||||
end
|
||||
end
|
||||
|
||||
describe "#extract_keyword!" do
|
||||
it "extracts and removes keyword from text" do
|
||||
text = "Project: test\nStatus: Open\nSome content here".dup
|
||||
result = subject.send(:extract_keyword!, text, :project, nil)
|
||||
expect(result).to eq("test")
|
||||
expect(text).not_to include("Project: test")
|
||||
end
|
||||
|
||||
it "handles case insensitive matching" do
|
||||
text = "PROJECT: test\nContent".dup
|
||||
result = subject.send(:extract_keyword!, text, :project, nil)
|
||||
expect(result).to eq("test")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#human_attr_translations" do
|
||||
let(:user) { build_stubbed(:user, language: "en") }
|
||||
|
||||
it "returns array of attribute translations" do
|
||||
result = subject.send(:human_attr_translations, :project)
|
||||
expect(result).to include("project", "Project")
|
||||
end
|
||||
|
||||
it "includes translations for user language" do
|
||||
allow(Setting).to receive(:default_language).and_return("en")
|
||||
result = subject.send(:human_attr_translations, :project)
|
||||
expect(result).to be_an(Array)
|
||||
expect(result).not_to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
describe "#ignored_filename?" do
|
||||
before do
|
||||
allow(Setting).to receive(:mail_handler_ignore_filenames).and_return("signature.asc\n*.tmp")
|
||||
end
|
||||
|
||||
it "returns true for ignored filenames" do
|
||||
expect(subject.send(:ignored_filename?, "signature.asc")).to be_truthy
|
||||
end
|
||||
|
||||
it "returns false for non-ignored filenames" do
|
||||
expect(subject.send(:ignored_filename?, "document.pdf")).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
describe "#lookup_case_insensitive_key" do
|
||||
let(:scope) { class_double(Status) }
|
||||
let(:status) { build_stubbed(:status) }
|
||||
let(:options) { { allow_override: [:status], issue: {} } }
|
||||
let(:plain_text_body) { "Status: resolved".dup }
|
||||
|
||||
before do
|
||||
allow(scope).to receive(:find_by).with("lower(name) = ?", "resolved").and_return(status)
|
||||
end
|
||||
|
||||
it "finds record case insensitively" do
|
||||
result = subject.send(:lookup_case_insensitive_key, scope, :status)
|
||||
expect(result).to eq(status.id)
|
||||
end
|
||||
|
||||
it "returns nil when keyword not found" do
|
||||
allow(scope).to receive(:find_by).and_return(nil)
|
||||
result = subject.send(:lookup_case_insensitive_key, scope, :status)
|
||||
expect(result).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe IncomingEmails::Handlers::MessageReply do
|
||||
let(:email) { instance_double(Mail::Message, attachments: [], subject: "Email subject") }
|
||||
let(:user) { build_stubbed(:user) }
|
||||
let(:reference) { {} }
|
||||
let(:options) { {} }
|
||||
let(:plain_text_body) { "Test body" }
|
||||
|
||||
subject(:handler) do
|
||||
described_class.new(email, user:, reference:, plain_text_body:, options:)
|
||||
end
|
||||
|
||||
describe ".handles?" do
|
||||
context "with message reference" do
|
||||
let(:reference) { { klass: "message", id: 123 } }
|
||||
|
||||
it "returns true" do
|
||||
expect(described_class).to be_handles(email, reference:)
|
||||
end
|
||||
end
|
||||
|
||||
context "without message reference" do
|
||||
let(:reference) { {} }
|
||||
|
||||
it "returns false" do
|
||||
expect(described_class).not_to be_handles(email, reference:)
|
||||
end
|
||||
end
|
||||
|
||||
context "with work package reference" do
|
||||
let(:reference) { { klass: "work_packages", id: 123 } }
|
||||
|
||||
it "returns false" do
|
||||
expect(described_class).not_to be_handles(email, reference:)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#process" do
|
||||
let(:project) { create(:project) }
|
||||
let(:forum) { create(:forum, project:) }
|
||||
let(:message) { create(:message, forum:) }
|
||||
let(:reference) { { klass: "messages", id: message.id } }
|
||||
|
||||
before do
|
||||
allow(Message).to receive(:find_by).with(id: message.id).and_return(message)
|
||||
end
|
||||
|
||||
context "when not allowed to create a message" do
|
||||
it "raises an exception" do
|
||||
expect { handler.process }.to raise_error(IncomingEmails::UnauthorizedAction)
|
||||
end
|
||||
end
|
||||
|
||||
context "when allowed to create a message" do
|
||||
before do
|
||||
mock_permissions_for(user) do |mock|
|
||||
mock.allow_in_project(:add_messages, project:)
|
||||
end
|
||||
end
|
||||
|
||||
it "creates a reply message" do
|
||||
call = handler.process
|
||||
expect(call).to be_a(ServiceResult)
|
||||
|
||||
message = call.result
|
||||
expect(message).to be_a(Message)
|
||||
end
|
||||
end
|
||||
|
||||
context "when parent message is not found" do
|
||||
before do
|
||||
allow(Message).to receive(:find_by).and_return(nil)
|
||||
end
|
||||
|
||||
it "returns nil" do
|
||||
result = handler.process
|
||||
expect(result).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,98 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe IncomingEmails::Handlers::WorkPackage do
|
||||
let(:email) { instance_double(Mail::Message, attachments: [], subject: "My message") }
|
||||
let(:user) { build_stubbed(:user) }
|
||||
let(:reference) { {} }
|
||||
let(:options) { { issue: { project: "foobar" }, allow_override: [] } }
|
||||
let(:plain_text_body) { "Test body".dup }
|
||||
|
||||
subject(:handler) do
|
||||
described_class.new(email, user:, reference:, plain_text_body:, options:)
|
||||
end
|
||||
|
||||
describe ".handles?" do
|
||||
context "with work package reference" do
|
||||
let(:reference) { { klass: "work_package", id: 123 } }
|
||||
|
||||
it "returns true" do
|
||||
expect(described_class).to be_handles(email, reference:)
|
||||
end
|
||||
end
|
||||
|
||||
context "without work package reference" do
|
||||
let(:reference) { {} }
|
||||
|
||||
it "returns true for new work package creation" do
|
||||
expect(described_class).to be_handles(email, reference:)
|
||||
end
|
||||
end
|
||||
|
||||
context "with message reference" do
|
||||
let(:reference) { { klass: "messages", id: 123 } }
|
||||
|
||||
it "returns false" do
|
||||
expect(described_class).not_to be_handles(email, reference:)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#process" do
|
||||
let(:service_result) { ServiceResult.success(result: build_stubbed(:work_package)) }
|
||||
let(:service_instance) { instance_double(WorkPackages::CreateService, call: service_result) }
|
||||
let(:project) { build_stubbed(:project) }
|
||||
|
||||
before do
|
||||
allow(WorkPackages::CreateService).to receive(:new).and_return(service_instance)
|
||||
allow(Project).to receive(:find_by).with(identifier: "foobar").and_return(project)
|
||||
end
|
||||
|
||||
it "creates a work package" do
|
||||
result = handler.process
|
||||
expect(WorkPackages::CreateService).to have_received(:new)
|
||||
expect(result).to be_a(ServiceResult)
|
||||
end
|
||||
|
||||
context "when work package creation fails" do
|
||||
let(:errors) { ActiveModel::Errors.new(nil) }
|
||||
let(:service_result) { ServiceResult.failure(errors:) }
|
||||
|
||||
it "returns the failed result" do
|
||||
result = handler.process
|
||||
expect(result).to be_a(ServiceResult)
|
||||
expect(result).not_to be_success
|
||||
expect(result.errors).to eq(errors)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,267 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe IncomingEmails::MailHandler, "integration", type: :service do
|
||||
# Integration tests to ensure backwards compatibility after refactoring
|
||||
|
||||
shared_let(:project) { create(:valid_project, identifier: "test-project") }
|
||||
shared_let(:user) { create(:user, mail: "test@example.com") }
|
||||
shared_let(:priority) { create(:priority_low, is_default: true) }
|
||||
|
||||
describe "MailHandler.receive" do
|
||||
let(:raw_email) do
|
||||
<<~EMAIL
|
||||
From: test@example.com
|
||||
To: project@example.com
|
||||
Subject: Test Work Package
|
||||
Content-Type: text/plain
|
||||
|
||||
Project: #{project.identifier}
|
||||
|
||||
This is a test work package created via email.
|
||||
EMAIL
|
||||
end
|
||||
|
||||
it "still processes emails through the old API" do
|
||||
result = described_class.receive(raw_email)
|
||||
|
||||
expect(result).to be_a(WorkPackage)
|
||||
expect(result.subject).to eq("Test Work Package")
|
||||
expect(result.project).to eq(project)
|
||||
expect(result.author).to eq(user)
|
||||
expect(result.description).to include("This is a test work package created via email.")
|
||||
end
|
||||
|
||||
it "handles options parameter" do
|
||||
result = described_class.receive(raw_email, { allow_override: "priority" })
|
||||
|
||||
expect(result).to be_a(WorkPackage)
|
||||
expect(result.project).to eq(project)
|
||||
end
|
||||
|
||||
context "with unknown user" do
|
||||
let(:unknown_email) do
|
||||
<<~EMAIL
|
||||
From: unknown@example.com
|
||||
To: project@example.com
|
||||
Subject: Test from unknown
|
||||
Content-Type: text/plain
|
||||
|
||||
Project: #{project.identifier}
|
||||
|
||||
This is from an unknown user.
|
||||
EMAIL
|
||||
end
|
||||
|
||||
it "handles unknown_user option, but does not skip permission checks" do
|
||||
result = described_class.receive(unknown_email, { unknown_user: "accept" })
|
||||
expect(result).to be_a(WorkPackage)
|
||||
expect(result.project).to eq(project)
|
||||
|
||||
expect(result.errors.symbols_for(:base)).to contain_exactly(:error_unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context "with auto-reply headers" do
|
||||
let(:auto_reply_email) do
|
||||
<<~EMAIL
|
||||
From: test@example.com
|
||||
To: project@example.com
|
||||
Subject: Test Auto Reply
|
||||
X-Auto-Response-Suppress: OOF
|
||||
Content-Type: text/plain
|
||||
|
||||
Project: #{project.identifier}
|
||||
|
||||
This is an auto-reply.
|
||||
EMAIL
|
||||
end
|
||||
|
||||
it "ignores auto-reply emails" do
|
||||
result = described_class.receive(auto_reply_email)
|
||||
|
||||
expect(result).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context "with work package reference" do
|
||||
let!(:work_package) { create(:work_package, project:) }
|
||||
let(:options) { {} }
|
||||
let(:reply_email) do
|
||||
<<~EMAIL
|
||||
From: test@example.com
|
||||
To: project@example.com
|
||||
Subject: Re: #{work_package.subject}
|
||||
References: <op.work_package-#{work_package.id}@example.com>
|
||||
Content-Type: text/plain
|
||||
|
||||
This is a reply to the work package.
|
||||
EMAIL
|
||||
end
|
||||
|
||||
subject { described_class.receive(reply_email, options) }
|
||||
|
||||
shared_examples "successful work package reply" do
|
||||
it "processes work package replies" do
|
||||
expect { subject }.to change(Journal, :count).by(1)
|
||||
|
||||
expect(subject).to be_a(Journal)
|
||||
expect(subject.journable).to eq(work_package)
|
||||
expect(subject.notes).to include("This is a reply to the work package.")
|
||||
end
|
||||
end
|
||||
|
||||
context "with permission" do
|
||||
let!(:role) { create(:project_role, permissions: %i[view_work_packages add_work_package_comments]) }
|
||||
let!(:member) { create(:member, project:, user:, roles: [role]) }
|
||||
|
||||
it_behaves_like "successful work package reply"
|
||||
end
|
||||
|
||||
context "without permission" do
|
||||
it "fails to process" do
|
||||
expect { subject }.not_to change(Journal, :count)
|
||||
end
|
||||
end
|
||||
|
||||
context "without permission, but no_permission_check set" do
|
||||
let(:options) do
|
||||
{ no_permission_check: true }
|
||||
end
|
||||
|
||||
it_behaves_like "successful work package reply"
|
||||
end
|
||||
end
|
||||
|
||||
context "with message reference" do
|
||||
let(:options) { {} }
|
||||
let!(:message) { create(:message, forum: create(:forum, project:)) }
|
||||
let(:message_reply_email) do
|
||||
<<~EMAIL
|
||||
From: test@example.com
|
||||
To: project@example.com
|
||||
Subject: Re: #{message.subject}
|
||||
References: <op.message-#{message.id}@example.com>
|
||||
Content-Type: text/plain
|
||||
|
||||
This is a reply to the message.
|
||||
EMAIL
|
||||
end
|
||||
|
||||
subject { described_class.receive(message_reply_email, options) }
|
||||
|
||||
shared_examples "successful message reply" do
|
||||
it "processes message replies" do
|
||||
expect { subject }.to change(Message, :count).by(1)
|
||||
|
||||
expect(subject).to be_a(Message)
|
||||
expect(subject.parent).to eq(message)
|
||||
expect(subject.content).to include("This is a reply to the message.")
|
||||
end
|
||||
end
|
||||
|
||||
context "with permission" do
|
||||
let!(:role) { create(:project_role, permissions: [:add_messages]) }
|
||||
let!(:member) { create(:member, project:, user:, roles: [role]) }
|
||||
|
||||
it_behaves_like "successful message reply"
|
||||
end
|
||||
|
||||
context "without permission" do
|
||||
it "fails to process" do
|
||||
expect { subject }.not_to change(Message, :count)
|
||||
expect(subject).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context "without permission, but no_permission_check set" do
|
||||
let(:options) do
|
||||
{ no_permission_check: true }
|
||||
end
|
||||
|
||||
it_behaves_like "successful message reply"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "Error handling" do
|
||||
let(:invalid_email) do
|
||||
<<~EMAIL
|
||||
From: test@example.com
|
||||
To: project@example.com
|
||||
Subject: Invalid Email
|
||||
Content-Type: text/plain
|
||||
|
||||
Project: nonexistent-project
|
||||
|
||||
This references a nonexistent project.
|
||||
EMAIL
|
||||
end
|
||||
|
||||
it "handles errors gracefully" do
|
||||
expect { described_class.receive(invalid_email) }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
describe "Handler registration" do
|
||||
let(:custom_handler) do
|
||||
Class.new(IncomingEmails::Handlers::Base) do
|
||||
def self.handles?(email, **)
|
||||
email.subject&.include?("CUSTOM")
|
||||
end
|
||||
|
||||
def process
|
||||
ServiceResult.success(message: "Custom handler processed", result: "foo result")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
let(:custom_email) do
|
||||
<<~EMAIL
|
||||
From: test@example.com
|
||||
To: project@example.com
|
||||
Subject: CUSTOM Test Email
|
||||
Content-Type: text/plain
|
||||
|
||||
This should be handled by the custom handler.
|
||||
EMAIL
|
||||
end
|
||||
|
||||
it "allows custom handlers to be registered" do
|
||||
IncomingEmails::DispatchService.register_handler(custom_handler)
|
||||
|
||||
result = described_class.receive(custom_email.dup)
|
||||
|
||||
expect(result).to eq("foo result")
|
||||
end
|
||||
end
|
||||
end
|
||||
+1
-1
@@ -28,7 +28,7 @@
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe MailHandler::UserCreator do
|
||||
RSpec.describe IncomingEmails::UserCreator do
|
||||
describe ".new_user_from_attributes" do
|
||||
context "with sufficient information" do
|
||||
# [address, name] => [login, firstname, lastname]
|
||||
Reference in New Issue
Block a user