diff --git a/app/controllers/spree/base_controller.rb b/app/controllers/spree/base_controller.rb new file mode 100644 index 0000000000..4d20570e20 --- /dev/null +++ b/app/controllers/spree/base_controller.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require 'cancan' + +module Spree + class BaseController < ApplicationController + include Spree::Core::ControllerHelpers::Auth + include Spree::Core::ControllerHelpers::RespondWith + include Spree::Core::ControllerHelpers::SSL + include Spree::Core::ControllerHelpers::Common + + respond_to :html + end +end + +require 'spree/i18n/initializer' diff --git a/lib/spree/core.rb b/lib/spree/core.rb new file mode 100644 index 0000000000..5a7d98dbd2 --- /dev/null +++ b/lib/spree/core.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require 'rails/all' +require 'active_merchant' +require 'acts_as_list' +require 'awesome_nested_set' +require 'cancan' +require 'kaminari' +require 'mail' +require 'paperclip' +require 'paranoia' +require 'ransack' +require 'state_machine' + +module Spree + mattr_accessor :user_class + + def self.user_class + if @@user_class.is_a?(Class) + raise "Spree.user_class MUST be a String object, not a Class object." + end + + return unless @@user_class.is_a?(String) + + @@user_class.constantize + end + + # Used to configure Spree. + # + # Example: + # + # Spree.config do |config| + # config.site_name = "An awesome Spree site" + # end + # + # This method is defined within the core gem on purpose. + # Some people may only wish to use the Core part of Spree. + def self.config + yield(Spree::Config) + end +end + +require 'spree/core/version' +require 'spree/core/engine' + +require 'spree/i18n' +require 'spree/money' + +require 'spree/core/delegate_belongs_to' +require 'spree/core/ext/active_record' +require 'spree/core/permalinks' +require 'spree/core/token_resource' +require 'spree/core/calculated_adjustments' +require 'spree/core/product_duplicator' + +ActiveRecord::Base.class_eval do + include CollectiveIdea::Acts::NestedSet +end diff --git a/lib/spree/core/calculated_adjustments.rb b/lib/spree/core/calculated_adjustments.rb new file mode 100644 index 0000000000..c502c00099 --- /dev/null +++ b/lib/spree/core/calculated_adjustments.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +module Spree + module Core + module CalculatedAdjustments + def self.included(klass) + klass.class_eval do + has_one :calculator, class_name: "Spree::Calculator", as: :calculable, dependent: :destroy + accepts_nested_attributes_for :calculator + validates :calculator, presence: true + + def self.calculators + spree_calculators.__send__(model_name_without_spree_namespace) + end + + def calculator_type + calculator.class.to_s if calculator + end + + def calculator_type=(calculator_type) + klass = calculator_type.constantize if calculator_type + self.calculator = klass.new if klass && !calculator.is_a?(klass) + end + + # Creates a new adjustment for the target object + # (which is any class that has_many :adjustments) and sets amount based on the + # calculator as applied to the given calculable (Order, LineItems[], Shipment, etc.) + # By default the adjustment will not be considered mandatory + def create_adjustment(label, target, calculable, mandatory = false, state = "closed") + # Adjustment calculations done on Spree::Shipment objects MUST + # be done on their to_package'd variants instead + # It's only the package that contains the correct information. + # See https://github.com/spree/spree_active_shipping/pull/96 et. al + old_calculable = calculable + calculable = calculable.to_package if calculable.is_a?(Spree::Shipment) + amount = compute_amount(calculable) + return if amount.zero? && !mandatory + + target.adjustments.create( + amount: amount, + source: old_calculable, + originator: self, + label: label, + mandatory: mandatory, + state: state + ) + end + + # Updates the amount of the adjustment using our Calculator and + # calling the +compute+ method with the +calculable+ + # referenced passed to the method. + def update_adjustment(adjustment, calculable) + # Adjustment calculations done on Spree::Shipment objects MUST + # be done on their to_package'd variants instead + # It's only the package that contains the correct information. + # See https://github.com/spree/spree_active_shipping/pull/96 et. al + calculable = calculable.to_package if calculable.is_a?(Spree::Shipment) + adjustment.update_column(:amount, compute_amount(calculable)) + end + + # Calculate the amount to be used when creating an adjustment + # NOTE: May be overriden by classes where this module is included into. + # Such as Spree::Promotion::Action::CreateAdjustment. + def compute_amount(calculable) + calculator.compute(calculable) + end + + def self.model_name_without_spree_namespace + to_s.tableize.gsub('/', '_').sub('spree_', '') + end + private_class_method :model_name_without_spree_namespace + + def self.spree_calculators + Rails.application.config.spree.calculators + end + private_class_method :spree_calculators + end + end + end + end +end diff --git a/lib/spree/core/delegate_belongs_to.rb b/lib/spree/core/delegate_belongs_to.rb new file mode 100644 index 0000000000..bfbf0b1fc0 --- /dev/null +++ b/lib/spree/core/delegate_belongs_to.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +## +# Creates methods on object which delegate to an association proxy. +# see delegate_belongs_to for two uses +# +# Todo - integrate with ActiveRecord::Dirty to make sure changes to delegate object are noticed +# Should do +# class User < ActiveRecord::Base; delegate_belongs_to :contact, :firstname; end +# class Contact < ActiveRecord::Base; end +# u = User.first +# u.changed? # => false +# u.firstname = 'Bobby' +# u.changed? # => true +# +# Right now the second call to changed? would return false +# +# Todo - add has_one support. fairly straightforward addition +## +module DelegateBelongsTo + extend ActiveSupport::Concern + + module ClassMethods + @@default_rejected_delegate_columns = ['created_at', 'created_on', 'updated_at', + 'updated_on', 'lock_version', 'type', 'id', + 'position', 'parent_id', 'lft', 'rgt'] + mattr_accessor :default_rejected_delegate_columns + + ## + # Creates methods for accessing and setting attributes on an association. Uses same + # default list of attributes as delegates_to_association. + # delegate_belongs_to :contact + # delegate_belongs_to :contact, [:defaults] ## same as above, and useless + # delegate_belongs_to :contact, [:defaults, :address, :fullname], :class_name => 'VCard' + ## + def delegate_belongs_to(association, *attrs) + opts = attrs.extract_options! + initialize_association :belongs_to, association, opts + attrs = get_association_column_names(association) if attrs.empty? + attrs.concat get_association_column_names(association) if attrs.delete :defaults + attrs.each do |attr| + class_def attr do |*args| + if args.empty? + __send__(:delegator_for, association).__send__(attr) + else + __send__(:delegator_for, association).__send__(attr, *args) + end + end + class_def "#{attr}=" do |val| + __send__(:delegator_for, association).__send__("#{attr}=", val) + end + end + end + + protected + + def get_association_column_names(association, without_default_rejected_delegate_columns = true) + association_klass = reflect_on_association(association).klass + methods = association_klass.column_names + if without_default_rejected_delegate_columns + methods.reject!{ |x| default_rejected_delegate_columns.include?(x.to_s) } + end + methods + rescue + [] + end + + ## + # initialize_association :belongs_to, :contact + def initialize_association(type, association, opts = {}) + unless [:belongs_to].include?(type.to_s.to_sym) + raise 'Illegal or unimplemented association type.' + end + + __send__(type, association, opts) if reflect_on_association(association).nil? + end + + private + + def class_def(name, method = nil, &blk) + class_eval { method.nil? ? define_method(name, &blk) : define_method(name, method) } + end + end + + def delegator_for(association) + if __send__(association).nil? + __send__("#{association}=", self.class.reflect_on_association(association).klass.new) + end + __send__(association) + end + protected :delegator_for +end + +ActiveRecord::Base.include(DelegateBelongsTo) diff --git a/lib/spree/core/environment/calculators.rb b/lib/spree/core/environment/calculators.rb new file mode 100644 index 0000000000..a5f60555f7 --- /dev/null +++ b/lib/spree/core/environment/calculators.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Spree + module Core + class Environment + class Calculators + include EnvironmentExtension + + attr_accessor :shipping_methods, :tax_rates + end + end + end +end diff --git a/lib/spree/core/environment_extension.rb b/lib/spree/core/environment_extension.rb new file mode 100644 index 0000000000..7b05908752 --- /dev/null +++ b/lib/spree/core/environment_extension.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Spree + module Core + module EnvironmentExtension + extend ActiveSupport::Concern + + def add_class(name) + instance_variable_set "@#{name}", Set.new + + create_method( "#{name}=".to_sym ) { |val| + instance_variable_set( "@" + name, val) + } + + create_method(name.to_sym) do + instance_variable_get( "@" + name ) + end + end + + private + + def create_method(name, &block) + self.class.__send__(:define_method, name, &block) + end + end + end +end diff --git a/lib/spree/core/gateway_error.rb b/lib/spree/core/gateway_error.rb new file mode 100644 index 0000000000..f90310bc54 --- /dev/null +++ b/lib/spree/core/gateway_error.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Spree + module Core + class GatewayError < RuntimeError; end + end +end diff --git a/lib/spree/core/mail_interceptor.rb b/lib/spree/core/mail_interceptor.rb new file mode 100644 index 0000000000..87ae2e33ab --- /dev/null +++ b/lib/spree/core/mail_interceptor.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +# Allows us to intercept any outbound mail message and make last minute changes +# (such as specifying a "from" address or sending to a test email account) +# +# See http://railscasts.com/episodes/206-action-mailer-in-rails-3 for more details. +module Spree + module Core + class MailInterceptor + def self.delivering_email(message) + return unless MailSettings.override? + + if Config[:intercept_email].present? + message.subject = "#{message.to} #{message.subject}" + message.to = Config[:intercept_email] + end + + return if Config[:mail_bcc].blank? + + message.bcc ||= Config[:mail_bcc] + end + end + end +end diff --git a/lib/spree/core/mail_settings.rb b/lib/spree/core/mail_settings.rb new file mode 100644 index 0000000000..80664d1c64 --- /dev/null +++ b/lib/spree/core/mail_settings.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +module Spree + module Core + class MailSettings + MAIL_AUTH = ['None', 'plain', 'login', 'cram_md5'].freeze + SECURE_CONNECTION_TYPES = ['None', 'SSL', 'TLS'].freeze + + # Override the Rails application mail settings based on preferences + # This makes it possible to configure the mail settings through an admin + # interface instead of requiring changes to the Rails envrionment file + def self.init + new.override! if override? + end + + def self.override? + Config.override_actionmailer_config + end + + def override! + if Config.enable_mail_delivery + ActionMailer::Base.default_url_options[:host] ||= Config.site_url + ActionMailer::Base.smtp_settings = mail_server_settings + ActionMailer::Base.perform_deliveries = true + else + ActionMailer::Base.perform_deliveries = false + end + end + + private + + def mail_server_settings + settings = if need_authentication? + basic_settings.merge(user_credentials) + else + basic_settings + end + + settings.merge(enable_starttls_auto: secure_connection?) + end + + def user_credentials + { user_name: Config.smtp_username, + password: Config.smtp_password } + end + + def basic_settings + { address: Config.mail_host, + domain: Config.mail_domain, + port: Config.mail_port, + authentication: Config.mail_auth_type } + end + + def need_authentication? + Config.mail_auth_type != 'None' + end + + def secure_connection? + Config.secure_connection_type == 'TLS' + end + end + end +end diff --git a/lib/spree/core/permalinks.rb b/lib/spree/core/permalinks.rb new file mode 100644 index 0000000000..b9d8a83274 --- /dev/null +++ b/lib/spree/core/permalinks.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +require 'stringex' + +module Spree + module Core + module Permalinks + extend ActiveSupport::Concern + + included do + class_attribute :permalink_options + end + + module ClassMethods + def make_permalink(options = {}) + options[:field] ||= :permalink + self.permalink_options = options + + return unless connected? && + table_exists? && + column_names.include?(permalink_options[:field].to_s) + + before_validation(on: :create) { save_permalink } + end + + def find_by_param(value, *args) + __send__("find_by_#{permalink_field}", value, *args) + end + + def find_by_param!(value, *args) + __send__("find_by_#{permalink_field}!", value, *args) + end + + def permalink_field + permalink_options[:field] + end + + def permalink_prefix + permalink_options[:prefix] || "" + end + + def permalink_order + order = permalink_options[:order] + "#{order} ASC," if order + end + end + + def generate_permalink + "#{self.class.permalink_prefix}#{Array.new(9) { rand(9) }.join}" + end + + def save_permalink(permalink_value = to_param) + with_lock do + permalink_value ||= generate_permalink + + field = self.class.permalink_field + + # Do other links exist with this permalink? + other = self.class. + where("#{self.class.table_name}.#{field} LIKE ?", "#{permalink_value}%") + if other.any? + # Find the existing permalink with the highest number, and increment that number. + # (If none of the existing permalinks have a number, this will evaluate to 1.) + number = other.map { |o| o.__send__(field)[/-(\d+)$/, 1].to_i }.max + 1 + permalink_value += "-#{number}" + end + write_attribute(field, permalink_value) + end + end + end + end +end + +ActiveRecord::Base.include(Spree::Core::Permalinks) +ActiveRecord::Relation.include(Spree::Core::Permalinks) diff --git a/lib/spree/core/s3_support.rb b/lib/spree/core/s3_support.rb new file mode 100644 index 0000000000..92229c15f5 --- /dev/null +++ b/lib/spree/core/s3_support.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Spree + module Core + # This module exists to reduce duplication in S3 settings between + # the Image and Taxon models in Spree + module S3Support + extend ActiveSupport::Concern + + included do + def self.supports_s3(field) + # Load user defined paperclip settings + config = Spree::Config + return unless config[:use_s3] + + s3_creds = { access_key_id: config[:s3_access_key], + secret_access_key: config[:s3_secret], + bucket: config[:s3_bucket] } + attachment_definitions[field][:storage] = :s3 + attachment_definitions[field][:s3_credentials] = s3_creds + attachment_definitions[field][:s3_headers] = ActiveSupport::JSON. + decode(config[:s3_headers]) + attachment_definitions[field][:bucket] = config[:s3_bucket] + if config[:s3_protocol].present? + attachment_definitions[field][:s3_protocol] = config[:s3_protocol].downcase + end + + return if config[:s3_host_alias].blank? + + attachment_definitions[field][:s3_host_alias] = config[:s3_host_alias] + end + end + end + end +end diff --git a/lib/spree/core/token_resource.rb b/lib/spree/core/token_resource.rb new file mode 100644 index 0000000000..8d4173d11b --- /dev/null +++ b/lib/spree/core/token_resource.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Spree + module Core + module TokenResource + module ClassMethods + def token_resource + has_one :tokenized_permission, as: :permissable + delegate :token, to: :tokenized_permission, allow_nil: true + after_create :create_token + end + end + + def create_token + permission = build_tokenized_permission + permission.token = token = ::SecureRandom.hex(8) + permission.save! + token + end + + def self.included(receiver) + receiver.extend ClassMethods + end + end + end +end + +ActiveRecord::Base.class_eval { include Spree::Core::TokenResource } diff --git a/lib/spree/i18n.rb b/lib/spree/i18n.rb new file mode 100644 index 0000000000..7780bc3ad7 --- /dev/null +++ b/lib/spree/i18n.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'i18n' +require 'active_support/core_ext/array/extract_options' +require 'spree/i18n/base' + +module Spree + extend ActionView::Helpers::TranslationHelper + + class << self + # Add spree namespace and delegate to Rails TranslationHelper for some nice + # extra functionality. e.g return reasonable strings for missing translations + def translate(*args) + @virtual_path = virtual_path + + options = args.extract_options! + options[:scope] = [*options[:scope]].unshift(:spree) + args << options + super(*args) + end + + alias_method :t, :translate + + def context + Spree::ViewContext.context + end + + def virtual_path + return unless context + + path = context.instance_variable_get("@virtual_path") + + return unless path + + path.gsub(/spree/, '') + end + end +end diff --git a/lib/spree/i18n/base.rb b/lib/spree/i18n/base.rb new file mode 100644 index 0000000000..88c2d3106d --- /dev/null +++ b/lib/spree/i18n/base.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Spree + module ViewContext + def self.context=(context) + @context = context + end + + def self.context + @context + end + + def view_context + super.tap do |context| + Spree::ViewContext.context = context + end + end + end +end diff --git a/lib/spree/i18n/initializer.rb b/lib/spree/i18n/initializer.rb new file mode 100644 index 0000000000..64b649f708 --- /dev/null +++ b/lib/spree/i18n/initializer.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +Spree::BaseController.include(Spree::ViewContext) diff --git a/lib/spree/money.rb b/lib/spree/money.rb new file mode 100644 index 0000000000..413c2f0e8c --- /dev/null +++ b/lib/spree/money.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: false + +require 'money' + +module Spree + class Money + attr_reader :money + + delegate :cents, to: :money + + def initialize(amount, options = {}) + @money = ::Money.parse([amount, (options[:currency] || Spree::Config[:currency])].join) + @options = {} + @options[:with_currency] = Spree::Config[:display_currency] + @options[:symbol_position] = Spree::Config[:currency_symbol_position].to_sym + @options[:no_cents] = Spree::Config[:hide_cents] + @options[:decimal_mark] = Spree::Config[:currency_decimal_mark] + @options[:thousands_separator] = Spree::Config[:currency_thousands_separator] + @options.merge!(options) + # Must be a symbol because the Money gem doesn't do the conversion + @options[:symbol_position] = @options[:symbol_position].to_sym + end + + def to_s + @money.format(@options) + end + + def to_html(options = { html: true }) + output = @money.format(@options.merge(options)) + if options[:html] + # 1) prevent blank, breaking spaces + # 2) prevent escaping of HTML character entities + output = output.gsub(" ", " ").html_safe + end + output + end + + def ==(other) + @money == other.money + end + end +end diff --git a/lib/spree/product_duplicator.rb b/lib/spree/product_duplicator.rb new file mode 100644 index 0000000000..8866f85fa5 --- /dev/null +++ b/lib/spree/product_duplicator.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +module Spree + class ProductDuplicator + attr_accessor :product + + def initialize(product) + @product = product + end + + def duplicate + new_product = duplicate_product + + # don't dup the actual variants, just the characterising types + new_product.option_types = product.option_types if product.has_variants? + + # allow site to do some customization + new_product.__send__(:duplicate_extra, product) if new_product.respond_to?(:duplicate_extra) + new_product.save! + new_product + end + + protected + + def duplicate_product + product.dup.tap do |new_product| + new_product.name = "COPY OF #{product.name}" + new_product.taxons = product.taxons + new_product.created_at = nil + new_product.deleted_at = nil + new_product.updated_at = nil + new_product.product_properties = reset_properties + new_product.master = duplicate_master + end + end + + def duplicate_master + master = product.master + master.dup.tap do |new_master| + new_master.sku = "COPY OF #{master.sku}" + new_master.deleted_at = nil + new_master.images = master.images.map { |image| duplicate_image image } + new_master.price = master.price + new_master.currency = master.currency + end + end + + def duplicate_image(image) + new_image = image.dup + new_image.assign_attributes(attachment: image.attachment.clone) + new_image + end + + def reset_properties + product.product_properties.map do |prop| + prop.dup.tap do |new_prop| + new_prop.created_at = nil + new_prop.updated_at = nil + end + end + end + end +end diff --git a/lib/spree/responder.rb b/lib/spree/responder.rb new file mode 100644 index 0000000000..3c0e60aa5b --- /dev/null +++ b/lib/spree/responder.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module Spree + class Responder < ::ActionController::Responder #:nodoc: + attr_accessor :on_success, :on_failure + + def initialize(controller, resources, options = {}) + super + + class_name = controller.class.name.to_sym + action_name = options.delete(:action_name) + + result = Spree::BaseController.spree_responders[class_name]. + try(:[], action_name). + try(:[], self.format.to_sym) + return unless result + + self.on_success = handler(controller, result, :success) + self.on_failure = handler(controller, result, :failure) + end + + def to_html + if !(on_success || on_failure) + super + return + end + + has_errors? ? controller.instance_exec(&on_failure) : controller.instance_exec(&on_success) + end + + def to_format + if !(on_success || on_failure) + super + return + end + + has_errors? ? controller.instance_exec(&on_failure) : controller.instance_exec(&on_success) + end + + private + + def handler(controller, result, status) + return result if result.respond_to? :call + + case result + when Hash + if result[status].is_a? Symbol + controller.method(result[status]) + else + result[status] + end + when Symbol + controller.method(result) + end + end + end +end diff --git a/spec/lib/spree/core/calculated_adjustments_spec.rb b/spec/lib/spree/core/calculated_adjustments_spec.rb new file mode 100644 index 0000000000..b9210c4f29 --- /dev/null +++ b/spec/lib/spree/core/calculated_adjustments_spec.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +require 'spec_helper' + +# Its pretty difficult to test this module in isolation b/c it needs to work in conjunction +# with an actual class that extends ActiveRecord::Base and has a corresponding table in the DB. +# So we'll just test it using Order and ShippingMethod. These classes are including the module. +describe Spree::Core::CalculatedAdjustments do + let(:calculator) { build(:calculator) } + let(:tax_rate) { Spree::TaxRate.new(calculator: calculator) } + + before do + allow(calculator).to receive(:compute) { 10 } + allow(calculator).to receive(:[]) { nil } + end + + it "should add has_one :calculator relationship" do + assert Spree::ShippingMethod. + reflect_on_all_associations(:has_one).map(&:name).include?(:calculator) + end + + context "#create_adjustment and its resulting adjustment" do + let(:order) { Spree::Order.create } + let(:target) { order } + + it "should be associated with the target" do + expect(target.adjustments).to receive(:create) + tax_rate.create_adjustment("foo", target, order) + end + + it "should have the correct originator and an amount derived from the calculator and supplied calculable" do + adjustment = tax_rate.create_adjustment("foo", target, order) + expect(adjustment).not_to be_nil + expect(adjustment.amount).to eq 10 + expect(adjustment.source).to eq order + expect(adjustment.originator).to eq tax_rate + end + + it "should be mandatory if true is supplied for that parameter" do + adjustment = tax_rate.create_adjustment("foo", target, order, true) + expect(adjustment).to be_mandatory + end + + context "when the calculator returns 0" do + before { allow(calculator).to receive_messages(compute: 0) } + + context "when adjustment is mandatory" do + before { tax_rate.create_adjustment("foo", target, order, true) } + + it "should create an adjustment" do + expect(Spree::Adjustment.count).to eq 1 + end + end + + context "when adjustment is not mandatory" do + before { tax_rate.create_adjustment("foo", target, order, false) } + + it "should not create an adjustment" do + expect(Spree::Adjustment.count).to eq 0 + end + end + end + end + + context "#update_adjustment" do + it "should update the adjustment using its calculator (and the specified source)" do + adjustment = double(:adjustment).as_null_object + calculable = double :calculable + expect(adjustment).to receive(:update_column).with(:amount, 10) + tax_rate.update_adjustment(adjustment, calculable) + end + end +end diff --git a/spec/lib/spree/core/mail_interceptor_spec.rb b/spec/lib/spree/core/mail_interceptor_spec.rb new file mode 100644 index 0000000000..d47538cb3c --- /dev/null +++ b/spec/lib/spree/core/mail_interceptor_spec.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +require 'spec_helper' + +# We'll use the OrderMailer as a quick and easy way to test. IF it works here +# it works for all email (in theory.) +describe Spree::OrderMailer do + let(:order) { Spree::Order.new(email: "customer@example.com") } + let(:message) { Spree::OrderMailer.confirm_email(order) } + + before(:all) do + ActionMailer::Base.perform_deliveries = true + ActionMailer::Base.deliveries.clear + end + + context "#deliver" do + before do + ActionMailer::Base.delivery_method = :test + end + + after { ActionMailer::Base.deliveries.clear } + + it "should use the from address specified in the preference" do + Spree::Config[:mails_from] = "no-reply@foobar.com" + message.deliver + @email = ActionMailer::Base.deliveries.first + expect(@email.from).to eq ["no-reply@foobar.com"] + end + + it "should use the provided from address" do + Spree::Config[:mails_from] = "preference@foobar.com" + message.from = "override@foobar.com" + message.to = "test@test.com" + message.deliver + email = ActionMailer::Base.deliveries.first + expect(email.from).to eq ["override@foobar.com"] + expect(email.to).to eq ["test@test.com"] + end + + it "should add the bcc email when provided" do + Spree::Config[:mail_bcc] = "bcc-foo@foobar.com" + message.deliver + @email = ActionMailer::Base.deliveries.first + expect(@email.bcc).to eq ["bcc-foo@foobar.com"] + end + + context "when intercept_email is provided" do + it "should strip the bcc recipients" do + expect(message.bcc).to be_blank + end + + it "should strip the cc recipients" do + expect(message.cc).to be_blank + end + + it "should replace the receipient with the specified address" do + Spree::Config[:intercept_email] = "intercept@foobar.com" + message.deliver + @email = ActionMailer::Base.deliveries.first + expect(@email.to).to eq ["intercept@foobar.com"] + end + + it "should modify the subject to include the original email" do + Spree::Config[:intercept_email] = "intercept@foobar.com" + message.deliver + @email = ActionMailer::Base.deliveries.first + expect(@email.subject.match(/customer@example\.com/)).to be_truthy + end + end + + context "when intercept_mode is not provided" do + it "should not modify the recipient" do + Spree::Config[:intercept_email] = "" + message.deliver + @email = ActionMailer::Base.deliveries.first + expect(@email.to).to eq ["customer@example.com"] + end + end + end +end diff --git a/spec/lib/spree/core/mail_settings_spec.rb b/spec/lib/spree/core/mail_settings_spec.rb new file mode 100644 index 0000000000..a8db4d5eed --- /dev/null +++ b/spec/lib/spree/core/mail_settings_spec.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +require 'spec_helper' + +module Spree + module Core + describe MailSettings do + let!(:subject) { MailSettings.new } + + context "override option is true" do + before { Config.override_actionmailer_config = true } + + context "init" do + it "calls override!" do + expect(MailSettings).to receive(:new).and_return(subject) + expect(subject).to receive(:override!) + MailSettings.init + end + end + + context "enable delivery" do + before { Config.enable_mail_delivery = true } + + context "overrides appplication defaults" do + context "authentication method is none" do + before do + Config.mail_host = "smtp.example.com" + Config.mail_domain = "example.com" + Config.mail_port = 123 + Config.mail_auth_type = MailSettings::SECURE_CONNECTION_TYPES[0] + Config.smtp_username = "schof" + Config.smtp_password = "hellospree!" + Config.secure_connection_type = "TLS" + subject.override! + end + + it { expect(ActionMailer::Base.smtp_settings[:address]).to eq "smtp.example.com" } + it { expect(ActionMailer::Base.smtp_settings[:domain]).to eq "example.com" } + it { expect(ActionMailer::Base.smtp_settings[:port]).to eq 123 } + it { expect(ActionMailer::Base.smtp_settings[:authentication]).to eq "None" } + it { expect(ActionMailer::Base.smtp_settings[:enable_starttls_auto]).to be_truthy } + + it "doesnt touch user name config" do + expect(ActionMailer::Base.smtp_settings[:user_name]).to be_nil + end + + it "doesnt touch password config" do + expect(ActionMailer::Base.smtp_settings[:password]).to be_nil + end + end + end + + context "when mail_auth_type is other than none" do + before do + Config.mail_auth_type = "login" + Config.smtp_username = "schof" + Config.smtp_password = "hellospree!" + subject.override! + end + + context "overrides user credentials" do + it { expect(ActionMailer::Base.smtp_settings[:user_name]).to eq "schof" } + it { expect(ActionMailer::Base.smtp_settings[:password]).to eq "hellospree!" } + end + end + end + + context "do not enable delivery" do + before do + Config.enable_mail_delivery = false + subject.override! + end + + it { expect(ActionMailer::Base.perform_deliveries).to be_falsy } + end + end + + context "override option is false" do + before { Config.override_actionmailer_config = false } + + context "init" do + it "doesnt calls override!" do + expect(subject).not_to receive(:override!) + MailSettings.init + end + end + end + end + end +end diff --git a/spec/lib/spree/core/token_resource_spec.rb b/spec/lib/spree/core/token_resource_spec.rb new file mode 100644 index 0000000000..087c5b9350 --- /dev/null +++ b/spec/lib/spree/core/token_resource_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'spec_helper' + +# Its pretty difficult to test this module in isolation b/c it needs to work in conjunction +# with an actual class that extends ActiveRecord::Base and has a corresponding table in the DB. +# So we'll just test it using Order instead since it included the module. +describe Spree::Core::TokenResource do + let(:order) { Spree::Order.new } + let(:permission) { double(Spree::TokenizedPermission) } + + it 'should add has_one :tokenized_permission relationship' do + assert Spree::Order. + reflect_on_all_associations(:has_one).map(&:name).include?(:tokenized_permission) + end + + context '#token' do + it 'should return the token of the associated permission' do + allow(order).to receive_messages tokenized_permission: permission + allow(permission).to receive_messages token: 'foo' + expect(order.token).to eq 'foo' + end + + it 'should return nil if there is no associated permission' do + expect(order.token).to be_nil + end + end + + context '#create_token' do + it 'should create a randomized 16 character token' do + token = order.create_token + expect(token.size).to eq 16 + end + end +end diff --git a/spec/lib/spree/i18n_spec.rb b/spec/lib/spree/i18n_spec.rb new file mode 100644 index 0000000000..386dd9d2ec --- /dev/null +++ b/spec/lib/spree/i18n_spec.rb @@ -0,0 +1,127 @@ +# frozen_string_literal: true + +require 'rspec/expectations' +require 'spree/i18n' +require 'spree/testing_support/i18n' + +describe "i18n" do + before do + I18n.backend.store_translations( + :en, + { + spree: { + foo: "bar", + bar: { + foo: "bar within bar scope", + invalid: nil, + legacy_translation: "back in the day..." + }, + invalid: nil, + legacy_translation: "back in the day..." + } + } + ) + end + + it "translates within the spree scope" do + expect(Spree.normal_t(:foo)).to eql("bar") + expect(Spree.translate(:foo)).to eql("bar") + end + + it "translates within the spree scope using a path" do + allow(Spree).to receive(:virtual_path).and_return('bar') + + expect(Spree.normal_t('.legacy_translation')).to eql("back in the day...") + expect(Spree.translate('.legacy_translation')).to eql("back in the day...") + end + + it "raise error without any context when using a path" do + expect { + Spree.normal_t('.legacy_translation') + }.to raise_error + + expect { + Spree.translate('.legacy_translation') + }.to raise_error + end + + it "prepends a string scope" do + expect(Spree.normal_t(:foo, scope: "bar")).to eql("bar within bar scope") + end + + it "prepends to an array scope" do + expect(Spree.normal_t(:foo, scope: ["bar"])).to eql("bar within bar scope") + end + + it "returns two translations" do + expect(Spree.normal_t([:foo, 'bar.foo'])).to eql(["bar", "bar within bar scope"]) + end + + it "returns reasonable string for missing translations" do + expect(Spree.t(:missing_entry)).to include("