action service: execute javascript code with access to built-in functions (send email, create/update content entries, ...etc) + refactoring

did committed Mar 29, 2016
commit 0b31fa3327fbd8384f519639b0c2eb217dd7b716
Showing 26 changed files with 1220 additions and 147 deletions
Gemfile.lock +7 -0
@@ @@ -8,6 +8,7 @@ PATH
coffee-script (~> 2.4.1)
compass (~> 1.0.3)
dragonfly (~> 1.0.12)
+ duktape (~> 1.3.0.6)
haml (~> 4.0.6)
httparty (~> 0.13.6)
kramdown (~> 1.10.0)
@@ @@ -18,6 +19,7 @@ PATH
moneta (~> 0.8.0)
morphine (~> 0.1.1)
nokogiri (~> 1.6.7.2)
+ pony (~> 1.11)
rack-cache (~> 1.6.1)
rack-rewrite (~> 1.5.1)
rack_csrf (~> 2.5.0)
@@ @@ -80,6 +82,7 @@ GEM
addressable (~> 2.3)
multi_json (~> 1.0)
rack (>= 1.3.0)
+ duktape (1.3.0.6)
execjs (2.6.0)
fast_stack (0.1.0)
rake
@@ @@ -112,6 +115,8 @@ GEM
attr_extras (~> 4.4.0)
colorize
stringex (~> 2.6.0)
+ mail (2.6.3)
+ mime-types (>= 1.16, < 3)
memory_profiler (0.9.6)
method_source (0.8.2)
mime-types (2.6.2)
@@ @@ -130,6 +135,8 @@ GEM
nokogumbo (1.4.7)
nokogiri
origin (2.1.1)
+ pony (1.11)
+ mail (>= 2.0)
pry (0.10.3)
coderay (~> 1.1.0)
method_source (~> 0.8.1)
locomotive/steam/adapters/filesystem.rb b/lib/locomotive/steam/adapters/filesystem.rb +4 -0
@@ @@ -45,6 +45,10 @@ module Locomotive::Steam
entity
end
+ def update(mapper, scope, entity)
+ entity
+ end
+
def inc(mapper, entity, attribute, amount = 1)
entity.tap do
entity[attribute] ||= 0
locomotive/steam/adapters/mongodb.rb b/lib/locomotive/steam/adapters/mongodb.rb +4 -0
@@ @@ -34,6 +34,10 @@ module Locomotive::Steam
command(mapper).insert(entity)
end
+ def update(mapper, scope, entity)
+ command(mapper).update(entity)
+ end
+
def inc(mapper, entity, attribute, amount = 1)
command(mapper).inc(entity, attribute, amount)
end
locomotive/steam/adapters/mongodb/command.rb b/lib/locomotive/steam/adapters/mongodb/command.rb +8 -1
@@ @@ -20,6 +20,13 @@ module Locomotive::Steam
entity
end
+ def update(entity)
+ entity.tap do
+ serialized_entity = @mapper.serialize(entity)
+ @collection.find(_id: entity._id).update_one(serialized_entity)
+ end
+ end
+
def inc(entity, attribute, amount = 1)
entity.tap do
@collection.find(_id: entity._id).update_one('$inc' => { attribute => amount })
@@ @@ -29,7 +36,7 @@ module Locomotive::Steam
end
def delete(entity)
- @collection.find(_id: entity._id).delete_one if entity._id
+ @collection.find(_id: entity._id).delete_one
end
end
locomotive/steam/errors.rb b/lib/locomotive/steam/errors.rb +3 -0
@@ @@ -3,6 +3,9 @@ module Locomotive::Steam
class NoSiteException < ::Exception
end
+ class ActionException < ::Exception
+ end
+
class RenderError < ::StandardError
LINES_RANGE = 10
locomotive/steam/initializers/sprockets.rb b/lib/locomotive/steam/initializers/sprockets.rb +7 -0
@@ @@ -4,6 +4,13 @@ require 'coffee_script'
require 'compass'
require 'autoprefixer-rails'
+ require 'execjs'
+
+ # Force ExecJS to select the best engine based on the current configuration.
+ # It means that if, down the road, we load a different javascript engine,
+ # the ExecJS runtime won't be affected.
+ ExecJS.runtime
+
module Locomotive::Steam
class SprocketsEnvironment < ::Sprockets::Environment
locomotive/steam/middlewares/renderer.rb b/lib/locomotive/steam/middlewares/renderer.rb +2 -1
@@ @@ -53,7 +53,8 @@ module Locomotive::Steam
services: services,
repositories: services.repositories,
logger: Locomotive::Common::Logger,
- live_editing: !!env['steam.live_editing']
+ live_editing: !!env['steam.live_editing'],
+ session: request.session
}
end
locomotive/steam/models/entity.rb b/lib/locomotive/steam/models/entity.rb +5 -0
@@ @@ -39,6 +39,11 @@ module Locomotive::Steam
attributes[name]
end
+ def change(new_attributes)
+ attributes.merge!((new_attributes || {}).with_indifferent_access)
+ self
+ end
+
def serialize
attributes.dup
end
locomotive/steam/models/repository.rb b/lib/locomotive/steam/models/repository.rb +4 -0
@@ @@ -32,6 +32,10 @@ module Locomotive::Steam
adapter.create(mapper, scope, entity)
end
+ def update(entity)
+ adapter.update(mapper, scope, entity)
+ end
+
def inc(entity, attribute, amount = 1)
adapter.inc(mapper, entity, attribute, amount)
end
locomotive/steam/services.rb b/lib/locomotive/steam/services.rb +13 -1
@@ @@ -62,8 +62,16 @@ module Locomotive
Steam::SnippetFinderService.new(repositories.snippet)
end
+ register :action do
+ Steam::Action.new(current_site, email, content_entry)
+ end
+
+ register :content_entry do
+ Steam::ContentEntryService.new(repositories.content_type, repositories.content_entry, locale)
+ end
+
register :entry_submission do
- Steam::EntrySubmissionService.new(repositories.content_type, repositories.content_entry, locale)
+ Steam::EntrySubmissionService.new(content_entry)
end
register :liquid_parser do
@@ @@ -106,6 +114,10 @@ module Locomotive
Steam::TextileService.new
end
+ register :email do
+ Steam::EmailService.new(page_finder, liquid_parser, asset_host, configuration.mode == :test)
+ end
+
register :cache do
Steam::NoCacheService.new
end
locomotive/steam/services/action_service.rb b/lib/locomotive/steam/services/action_service.rb +98 -0
@@ @@ -0,0 +1,98 @@
+ # TODO: accessor to other services: email, content_entry CRUD actions
+
+ # built-in functions (prefix by a namespace? or included in a module):
+ # x site
+ # x sendEmail(params) => nil
+ # x setProp(key, value) # Liquid context (assigns)
+ # x getProp(key) # Liquid context (assigns)
+ # x setSessionProp(key, value)
+ # x getSessionProp(key)
+ # x allEntries(type, filters) => Array of hashes
+ # x findEntry(type, <id_or_slug>)
+ # - createEntry(<type>, attributes) => Hash (with id)
+ # - updateEntry(<type>, <id_or_slug>, attributes)
+
+ # liquid tag: {% action '<name>' %}<coffee script here>{% endaction %}
+
+ require 'duktape'
+
+ module Locomotive
+ module Steam
+
+ class ActionService
+
+ BUILT_IN_FUNCTIONS = %w(
+ getProp
+ sendProp
+ getSessionProp
+ setSessionProp
+ sendEmail
+ allEntries
+ findEntry
+ createEntry
+ updateEntry)
+
+ attr_accessor_initialize :site, :email, :content_entry_service
+
+ def run(script, params = {}, liquid_context)
+ context = Duktape::Context.new
+
+ define_built_in_functions(context, liquid_context)
+
+ context.exec_string <<-JS
+ function locomotiveAction(site, params) {
+ #{script}
+ }
+ JS
+
+ context.call_prop('locomotiveAction', site.as_json, params)
+ end
+
+ private
+
+ def define_built_in_functions(context, liquid_context)
+ BUILT_IN_FUNCTIONS.each do |name|
+ context.define_function name, &send(:"#{name.underscore}_lambda", liquid_context)
+ end
+ end
+
+ def send_email_lambda(liquid_context)
+ -> (options) { email.send_email(options, liquid_context) }
+ end
+
+ def get_prop_lambda(liquid_context)
+ -> (name) { liquid_context[name].as_json }
+ end
+
+ def send_prop_lambda(liquid_context)
+ -> (name, value) { liquid_context[name] = value }
+ end
+
+ def get_session_prop_lambda(liquid_context)
+ -> (name) { liquid_context.registers[:session][name.to_sym].as_json }
+ end
+
+ def set_session_prop_lambda(liquid_context)
+ -> (name, value) { liquid_context.registers[:session][name.to_sym] = value }
+ end
+
+ def all_entries_lambda(liquid_context)
+ -> (type, conditions) { content_entry_service.all(type, conditions, true) }
+ end
+
+ def find_entry_lambda(liquid_context)
+ -> (type, id_or_slug) { content_entry_service.find(type, id_or_slug, true) }
+ end
+
+ def create_entry_lambda(liquid_context)
+ -> (type, attributes) { content_entry_service.create(type, attributes, true) }
+ end
+
+ def update_entry_lambda(liquid_context)
+ -> (type, id_or_slug, attributes) { content_entry_service.update(type, id_or_slug, attributes, true) }
+ end
+
+ end
+
+ end
+ end
locomotive/steam/services/content_entry_service.rb b/lib/locomotive/steam/services/content_entry_service.rb +114 -0
@@ @@ -0,0 +1,114 @@
+ require 'sanitize'
+
+ module Locomotive
+ module Steam
+
+ class ContentEntryService
+
+ include Locomotive::Steam::Services::Concerns::Decorator
+
+ attr_accessor_initialize :content_type_repository, :repository, :locale
+
+ def all(type_slug, conditions = {}, as_json = false)
+ with_repository(type_slug) do |_repository|
+ _repository.all(conditions).map do |entry|
+ _decorate(entry, as_json)
+ end
+ end
+ end
+
+ def find(type_slug, id_or_slug, as_json = false)
+ with_repository(type_slug) do |_repository|
+ entry = _repository.by_slug(id_or_slug) || _repository.find(id_or_slug)
+ _decorate(entry, as_json)
+ end
+ end
+
+ # Warning: do not work with localized and file fields
+ def create(type_slug, attributes, as_json = false)
+ with_repository(type_slug) do |_repository|
+ entry = _repository.build(clean_attributes(attributes))
+ decorated_entry = i18n_decorate { entry }
+
+ if validate(_repository, decorated_entry)
+ _repository.create(entry)
+ end
+
+ _json_decorate(decorated_entry, as_json)
+ end
+ end
+
+ # Warning: do not work with localized and file fields
+ def update(type_slug, id_or_slug, attributes, as_json = false)
+ with_repository(type_slug) do |_repository|
+ entry = _repository.by_slug(id_or_slug) || _repository.find(id_or_slug)
+ decorated_entry = i18n_decorate { entry.change(clean_attributes(attributes)) }
+
+ if validate(_repository, decorated_entry)
+ _repository.update(entry)
+ end
+
+ _json_decorate(decorated_entry, as_json)
+ end
+ end
+
+ def delete(type_slug, id_or_slug)
+ with_repository(type_slug) do |_repository|
+ entry = _repository.by_slug(id_or_slug) || _repository.find(id_or_slug)
+ _repository.delete(entry)
+ end
+ end
+
+ def get_type(slug)
+ return nil if slug.blank?
+
+ content_type_repository.by_slug(slug)
+ end
+
+ private
+
+ def with_repository(type_or_slug)
+ type = type_or_slug.respond_to?(:fields) ? type_or_slug : get_type(type_or_slug)
+
+ return if type.nil?
+
+ yield(repository.with(type))
+ end
+
+ def _decorate(entry, as_json)
+ decorated_entry = i18n_decorate { entry }
+ _json_decorate(decorated_entry, as_json)
+ end
+
+ def _json_decorate(entry, as_json)
+ as_json ? entry.as_json : entry
+ end
+
+ def clean_attributes(attributes)
+ attributes.each do |key, value|
+ next unless value.is_a?(String)
+ attributes[key] = Sanitize.clean(value, Sanitize::Config::BASIC)
+ end
+ attributes
+ end
+
+ def validate(_repository, entry)
+ # simple validations (existence of values) first
+ entry.valid?
+
+ # check if the entry has unique values for its
+ # fields marked as unique
+ content_type_repository.look_for_unique_fields(entry.content_type).each do |name, _|
+ if _repository.exists?(name => entry.send(name))
+ entry.errors.add(name, :unique)
+ end
+ end
+
+ entry.errors.empty?
+ end
+
+ end
+
+ end
+ end
+
locomotive/steam/services/email_service.rb b/lib/locomotive/steam/services/email_service.rb +93 -0
@@ @@ -0,0 +1,93 @@
+ require 'pony'
+
+ module Locomotive
+ module Steam
+
+ class EmailService
+
+ attr_accessor_initialize :page_finder_service, :liquid_parser, :asset_host, :simulation
+
+ def send_email(options, context)
+ build_body(options, context, options.delete(:html))
+
+ extract_attachment(options)
+
+ options[:via] ||= :smtp
+ options[:via_options] ||= options.delete(:smtp)
+
+ log(options)
+
+ !simulation ? Pony.mail(options) : nil
+ end
+
+ def logger
+ Locomotive::Common::Logger
+ end
+
+ private
+
+ def build_body(options, context, html = true)
+ key = html || html.nil? ? :html_body : :body
+
+ document = (if handle = options.delete(:page_handle)
+ parse_page(handle)
+ elsif body = options.delete(:body)
+ liquid_parser.parse_string(body)
+ else
+ raise "[EmailService] the body or page_handle options are missing."
+ end)
+
+ options[key] = document.render(context)
+ end
+
+ def parse_page(handle)
+ if page = page_finder_service.by_handle(handle, false)
+ liquid_parser.parse(page) # the liquid parser decorates the page (i18n)
+ else
+ raise "[EmailService] No page found with the following handle: #{handle}"
+ end
+ end
+
+ def extract_attachment(options)
+ (options[:attachments] || {}).each do |filename, value|
+ options[:attachments][filename] = read_attachment(value)
+ end
+ end
+
+ def read_attachment(value)
+ url = case value
+ when /^https?:\/\// then value
+ when /^\// then asset_host.compute(value, false)
+ else
+ nil
+ end
+
+ url ? _read_http_attachment(url) : value
+ end
+
+ def _read_http_attachment(url)
+ begin
+ uri = URI(url)
+ Net::HTTP.get(uri)
+ rescue Exception => e
+ logger.error "[SendEmail] Unable to read the '#{url}' url, error: #{e.message}"
+ nil
+ end
+ end
+
+ def log(options)
+ message = ["Sent email via #{options[:via]} (#{options[:via_options].inspect}):"]
+ message << "From: #{options[:from]}"
+ message << "To: #{options[:to]}"
+ message << "Subject: #{options[:subject]}"
+ message << "-----------"
+ message << (options[:body] || options[:html_body]).gsub("\n", "\n\t")
+ message << "-----------"
+
+ logger.info message.join("\n") + "\n\n"
+ end
+
+ end
+
+ end
+ end
locomotive/steam/services/entry_submission_service.rb b/lib/locomotive/steam/services/entry_submission_service.rb +6 -58
@@ @@ -1,76 +1,24 @@
- require 'sanitize'
-
module Locomotive
module Steam
class EntrySubmissionService
- include Locomotive::Steam::Services::Concerns::Decorator
-
- attr_accessor_initialize :content_type_repository, :repository, :locale
+ attr_accessor_initialize :service
- def submit(slug, attributes = {})
- type = get_type(slug)
+ def submit(type_slug, attributes = {})
+ type = service.get_type(type_slug)
return nil if type.nil? || type.public_submission_enabled == false
- clean_attributes(attributes)
-
- build_entry(type, attributes) do |entry|
- if validate(entry)
- repository.create(entry)
- end
- end
+ service.create(type, attributes)
end
def find(type_slug, slug)
- type = get_type(type_slug)
-
- return nil if type.nil?
-
- i18n_decorate { repository.with(type).by_slug(slug) }
+ service.find(type_slug, slug)
end
def to_json(entry)
- return nil if entry.nil?
-
- entry.to_json
- end
-
- private
-
- def get_type(slug)
- return nil if slug.blank?
-
- content_type_repository.by_slug(slug)
- end
-
- def build_entry(type, attributes, &block)
- i18n_decorate { repository.with(type).build(attributes) }.tap do |entry|
- yield(entry)
- end
- end
-
- def validate(entry)
- # simple validations (existence of values) first
- entry.valid?
-
- # check if the entry has unique values for its
- # fields marked as unique
- content_type_repository.look_for_unique_fields(entry.content_type).each do |name, _|
- if repository.with(entry.content_type).exists?(name => entry.send(name))
- entry.errors.add(name, :unique)
- end
- end
-
- entry.errors.empty?
- end
-
- def clean_attributes(attributes)
- attributes.each do |key, value|
- next unless value.is_a?(String)
- attributes[key] = Sanitize.clean(value, Sanitize::Config::BASIC)
- end
+ entry.try(&:to_json)
end
end
locomotive/steam/services/liquid_parser_service.rb b/lib/locomotive/steam/services/liquid_parser_service.rb +6 -0
@@ @@ -14,6 +14,12 @@ module Locomotive
default_editable_content: {})
end
+ def parse_string(string)
+ Locomotive::Steam::Liquid::Template.parse(string,
+ snippet_finder: snippet_finder,
+ parser: self)
+ end
+
def _parse(object, options = {})
# Note: the template must not be parsed here
Locomotive::Steam::Liquid::Template.parse(object.liquid_source, options)
locomotivecms_steam.gemspec +2 -0
@@ @@ -44,6 +44,8 @@ Gem::Specification.new do |spec|
spec.add_dependency 'haml', '~> 4.0.6'
spec.add_dependency 'mimetype-fu', '~> 0.1.2'
spec.add_dependency 'mime-types', '~> 2.6.1'
+ spec.add_dependency 'duktape', '~> 1.3.0.6'
+ spec.add_dependency 'pony', '~> 1.11'
spec.add_dependency 'locomotivecms-solid', '~> 4.0.1'
spec.add_dependency 'locomotivecms_common', '~> 0.1.0'
spec/fixtures/default/data/messages.yml +0 -0
spec/integration/services/content_entry_service_spec.rb +110 -0
@@ @@ -0,0 +1,110 @@
+ require 'spec_helper'
+
+ require_relative '../../../lib/locomotive/steam/adapters/filesystem.rb'
+ require_relative '../../../lib/locomotive/steam/adapters/mongodb.rb'
+
+ describe Locomotive::Steam::ContentEntryService do
+
+ shared_examples_for 'a content entry service' do
+
+ let(:site) { Locomotive::Steam::Site.new(_id: site_id, locales: %w(en fr nb)) }
+ let(:locale) { :en }
+ let(:type_repository) { Locomotive::Steam::ContentTypeRepository.new(adapter, site, locale) }
+ let(:entry_repository) { Locomotive::Steam::ContentEntryRepository.new(adapter, site, locale, type_repository) }
+ let(:service) { described_class.new(type_repository, entry_repository, locale) }
+ let(:type) { 'bands' }
+
+ describe '#all' do
+ subject { service.all(type) }
+ it { expect(subject.size).to eq 3 }
+ context 'with conditions' do
+ subject { service.all(type, kind: 'grunge') }
+ it { expect(subject.size).to eq 2 }
+ end
+ context 'as_json enabled' do
+ subject { service.all(type, { kind: 'grunge' }, true) }
+ it { expect(subject.first.slice('name', 'leader')).to eq('name' => 'Alice in Chains', 'leader' => 'Layne') }
+ end
+ end
+
+ describe '#find' do
+ let(:id_or_slug) { 'alice-in-chains'}
+ subject { service.find(type, id_or_slug) }
+ it { expect(subject.name).to eq 'Alice in Chains' }
+ context 'with an id' do
+ let(:id_or_slug) { entry_id }
+ it { expect(subject.name).to eq 'Pearl Jam' }
+ end
+ end
+
+ end
+
+ context 'MongoDB' do
+
+ it_should_behave_like 'a content entry service' do
+
+ let(:site_id) { mongodb_site_id }
+ let(:adapter) { Locomotive::Steam::MongoDBAdapter.new(database: 'steam_test', hosts: ['127.0.0.1:27017']) }
+ let(:entry_id) { BSON::ObjectId.from_string('5610310b87f6431588000029') }
+
+ describe '#create' do
+ subject { service.create('messages', { name: 'John', email: 'john@doe.net', message: 'Hello world!' }) }
+ it { expect { subject }.to change { service.all('messages').size } }
+ it { expect(subject.name).to eq 'John' }
+ after { service.delete('messages', subject._id) }
+ end
+
+ describe '#update' do
+ let!(:message) { service.create('messages', { name: 'John', email: 'john@doe.net', message: 'Hello world!' }) }
+ subject { service.update('messages', message._id, { name: 'Jane' }) }
+ it { expect { subject }.not_to change { service.all('messages').size } }
+ it { expect(subject.name).to eq 'Jane' }
+ after { service.delete('messages', message._id) }
+ end
+
+ end
+
+ end
+
+ context 'Filesystem' do
+
+ it_should_behave_like 'a content entry service' do
+
+ let(:site_id) { 1 }
+ let(:adapter) { Locomotive::Steam::FilesystemAdapter.new(default_fixture_site_path) }
+ let(:entry_id) { 'pearl-jam' }
+
+ after(:all) { Locomotive::Steam::Adapters::Filesystem::SimpleCacheStore.new.clear }
+
+ describe '#create' do
+
+ let(:attributes) { { name: 'John', email: 'john@doe.net', message: 'Hello world!' } }
+
+ subject { service.create('messages', attributes, true) }
+
+ it { expect { subject }.to change { service.all('messages').size } }
+ it { expect(subject['name']).to eq 'John' }
+ it { expect(subject['errors'].blank?).to eq true }
+
+ context 'missing attributes' do
+
+ let(:attributes) { {} }
+
+ it { expect { subject }.not_to change { service.all('messages').size } }
+ it { expect(subject['errors']).to eq({ 'name' => ["can't be blank"], 'email' => ["can't be blank"], 'message' => ["can't be blank"] }) }
+
+ end
+ end
+
+ describe '#update' do
+ let!(:message) { service.create('messages', { name: 'John', email: 'john@doe.net', message: 'Hello world!' }) }
+ subject { service.update('messages', message._id, { name: 'Jane' }, true) }
+ it { expect { subject }.not_to change { service.all('messages').size } }
+ it { expect(subject['name']).to eq 'Jane' }
+ end
+
+ end
+
+ end
+
+ end
spec/unit/entities/content_entry_spec.rb +15 -0
@@ @@ -10,6 +10,21 @@ describe Locomotive::Steam::ContentEntry do
before { content_entry.content_type = type }
+ describe '#change' do
+
+ let(:fields) { [instance_double('Field', name: :title, type: :string, required: true)] }
+
+ before do
+ allow(type).to receive(:fields_by_name).and_return({ title: fields.first })
+ end
+
+ subject { content_entry.change('title' => 'Hello world!') }
+
+ it { expect(subject.title).to eq('Hello world!') }
+ it { expect(subject._slug).to eq('hello-world') }
+
+ end
+
describe '#valid?' do
let(:fields) { [instance_double('Field', name: :title, type: :string, required: true)] }
spec/unit/entities/editable_element_spec.rb +19 -0
@@ @@ -7,4 +7,23 @@ describe Locomotive::Steam::EditableElement do
it { expect(page.block).to eq nil }
+ describe '#source' do
+
+ let(:source) { 'Hello world' }
+ let(:attributes) { { content: 'Lorem ipsum', source: source } }
+
+ subject { page.source }
+
+ it { is_expected.to eq 'Hello world' }
+
+ context 'no source attribute' do
+
+ let(:source) { nil }
+
+ it { is_expected.to eq 'Lorem ipsum' }
+
+ end
+
+ end
+
end
spec/unit/entities/page_spec.rb +10 -0
@@ @@ -77,4 +77,14 @@ describe Locomotive::Steam::Page do
end
+ describe '#source' do
+
+ let(:attributes) { { 'raw_template' => 'template code here' } }
+
+ subject { page.source }
+
+ it { is_expected.to eq 'template code here'}
+
+ end
+
end
spec/unit/models/i18n_field_spec.rb +23 -0
@@ @@ -22,4 +22,27 @@ describe Locomotive::Steam::Models::I18nField do
end
+ describe '#dup' do
+
+ let(:translations) { { en: 'Hello world', fr: nil } }
+
+ subject { field.dup }
+
+ it 'gets a fresh copy of the translations' do
+ expect(subject[:en]).to eq 'Hello world'
+ expect(subject.translations.object_id).not_to eq field.translations.object_id
+ end
+
+ end
+
+ describe '#to_json' do
+
+ let(:translations) { { en: 'Hello world', fr: nil } }
+
+ subject { field.to_json }
+
+ it { is_expected.to eq("{\"en\":\"Hello world\",\"fr\":null}") }
+
+ end
+
end
spec/unit/services/action_service_spec.rb +173 -0
@@ @@ -0,0 +1,173 @@
+ require 'spec_helper'
+
+ describe Locomotive::Steam::ActionService do
+
+ let(:site_hash) { { 'name' => 'Acme Corp' } }
+ let(:site) { instance_double('Site', as_json: site_hash ) }
+ let(:email_service) { instance_double('EmailService') }
+ let(:entry_service) { instance_double('ContentService') }
+ let(:service) { described_class.new(site, email_service, entry_service) }
+
+ describe '#run' do
+
+ let(:script) { 'return 1 + 1;' }
+ let(:params) { {} }
+ let(:assigns) { {} }
+ let(:session) { {} }
+ let(:context) { ::Liquid::Context.new(assigns, {}, { session: session }) }
+
+ subject { service.run(script, params, context) }
+
+ it { is_expected.to eq 2.0 }
+
+ describe 'with params' do
+
+ let(:params) { { 'foo' => 'hello' } }
+ let(:script) { "return params.foo + ' world';" }
+
+ it { is_expected.to eq 'hello world' }
+
+ describe "messing with params" do
+
+ let(:script) { "params.foo += ' world!';" }
+
+ it { is_expected.to eq nil }
+
+ it "can't change a param value" do
+ subject
+ expect(params['foo']).to eq 'hello'
+ end
+
+ end
+
+ end
+
+ describe 'built-in functions / getters / setters' do
+
+ describe 'site' do
+
+ let(:script) { 'return "Name: " + site.name;' }
+
+ it { is_expected.to eq 'Name: Acme Corp' }
+
+ end
+
+ describe 'getProp' do
+
+ let(:assigns) { { 'name' => 'John' } }
+ let(:script) { "return getProp('name');" }
+
+ it { is_expected.to eq 'John' }
+
+ end
+
+ describe 'sendProp' do
+
+ let(:script) { "return sendProp('done', true);" }
+
+ it { subject; expect(context['done']).to eq true }
+
+ end
+
+ describe 'getSessionProp' do
+
+ let(:session) { { name: 'John' } }
+ let(:script) { "return getSessionProp('name');" }
+
+ it { is_expected.to eq 'John' }
+
+ end
+
+ describe 'sendSessionProp' do
+
+ let(:script) { "return setSessionProp('done', true);" }
+
+ it { subject; expect(session[:done]).to eq true }
+
+ end
+
+ describe 'allEntries' do
+
+ let(:now) { Time.use_zone('America/Chicago') { Time.zone.local(2015, 'mar', 25, 10, 0) } }
+ let(:assigns) { { 'now' => now } }
+ let(:script) {
+ <<-JS
+ var entries = allEntries('bands', { 'created_at.lte': getProp('now'), published: true });
+ var names = []
+
+ for (var i = 0; i < entries.length; i++) {
+ names.push(entries[i].name)
+ }
+
+ return names.join(', ')
+ JS
+ }
+
+ before do
+ expect(entry_service).to receive(:all).with('bands', { "created_at.lte" => "2015-03-25T10:00:00.000-05:00", "published" => true }, true).and_return([
+ { 'name' => 'Pearl Jam' },
+ { 'name' => 'Nirvana' },
+ { 'name' => 'Soundgarden' }
+ ])
+ end
+
+ it { is_expected.to eq('Pearl Jam, Nirvana, Soundgarden') }
+
+ end
+
+ describe 'findEntry' do
+
+ let(:script) { "return findEntry('bands', '42').name;" }
+
+ before do
+ expect(entry_service).to receive(:find).with('bands', '42', true).and_return('name' => 'Pearl Jam')
+ end
+
+ it { is_expected.to eq('Pearl Jam') }
+
+ end
+
+ describe 'createEntry' do
+
+ let(:script) { "return createEntry('bands', { name: 'Pearl Jam'}).name;" }
+
+ before do
+ expect(entry_service).to receive(:create).with('bands', { 'name' => 'Pearl Jam' }, true).and_return('name' => 'Pearl Jam')
+ end
+
+ it { is_expected.to eq('Pearl Jam') }
+
+ end
+
+ describe 'updateEntry' do
+
+ let(:script) { "return updateEntry('bands', 'pearl-jam', { name: 'Pearl Jam'}).name;" }
+
+ before do
+ expect(entry_service).to receive(:update).with('bands', 'pearl-jam', { 'name' => 'Pearl Jam' }, true).and_return('name' => 'Pearl Jam')
+ end
+
+ it { is_expected.to eq('Pearl Jam') }
+
+ end
+
+ describe 'sendEmail' do
+
+ let(:params) { { 'to' => 'estelle@locomotivecms.com' } }
+ let(:script) { "sendEmail({ to: params.to, from: 'did@locomotivecms.com', subject: 'Happy Easter' })" }
+
+ it 'forwards the action to the email service' do
+ expect(email_service).to receive(:send_email).with({
+ 'to' => 'estelle@locomotivecms.com',
+ 'from' => 'did@locomotivecms.com',
+ 'subject' => 'Happy Easter' }, context)
+ subject
+ end
+
+ end
+
+ end
+
+ end
+
+ end
spec/unit/services/content_entry_service_spec.rb +145 -0
@@ @@ -0,0 +1,145 @@
+ require 'spec_helper'
+
+ describe Locomotive::Steam::ContentEntryService do
+
+ let(:site) { instance_double('Site', default_locale: 'en') }
+ let(:locale) { 'en' }
+ let(:type_repository) { instance_double('ContentTypeRepository') }
+ let(:entry_repository) { instance_double('Repository', site: site, locale: locale, content_type_repository: type_repository) }
+ let(:service) { described_class.new(type_repository, entry_repository, locale) }
+
+ before { allow(entry_repository).to receive(:with).and_return(entry_repository) }
+
+ describe '#validate' do
+
+ let(:attributes) { { title: 'Hello world' } }
+ let(:unique_fields) { {} }
+ let(:first_validation) { false }
+ let(:errors) { [:title] }
+ let(:type) { instance_double('Comments') }
+ let(:entry) { instance_double('Entry', title: 'Hello world', content_type: type, valid?: first_validation, errors: errors, attributes: { title: 'Hello world' }, localized_attributes: []) }
+
+ before do
+ allow(type_repository).to receive(:by_slug).and_return(type)
+ allow(type_repository).to receive(:look_for_unique_fields).and_return(unique_fields)
+ allow(entry_repository).to receive(:build).with(attributes).and_return(entry)
+ end
+
+ subject { service.send(:validate, entry_repository, entry) }
+
+ context 'valid' do
+
+ let(:first_validation) { true }
+ let(:errors) { {} }
+
+ it { is_expected.to eq true }
+ it { subject; expect(entry.errors.empty?).to eq true }
+
+ end
+
+ context 'not valid' do
+
+ it { is_expected.to eq false }
+ it { subject; expect(entry.errors).to eq([:title]) }
+
+ context 'with unique fields' do
+
+ let(:unique_fields) { { title: instance_double('Field', name: 'title') } }
+
+ before do
+ allow(entry_repository).to receive(:exists?).with(title: 'Hello world').and_return(true)
+ expect(entry.errors).to receive(:add).with(:title, :unique).and_return(true)
+ end
+
+ it { is_expected.to eq false }
+ it { subject; expect(entry.errors).to eq([:title]) }
+
+ end
+
+ end
+
+ end
+
+ end
+
+ # describe '#submit' do
+
+ # let(:slug) { nil }
+ # let(:attributes) { { title: 'Hello world' } }
+ # subject { service.submit(slug, attributes) }
+
+ # it { is_expected.to eq nil }
+
+ # context 'unknown content type' do
+
+ # let(:slug) { 'articles' }
+
+ # before { allow(type_repository).to receive(:by_slug).with('articles').and_return nil }
+
+ # it { is_expected.to eq nil }
+
+ # end
+
+ # context 'existing content type' do
+
+ # let(:unique_fields) { {} }
+ # let(:first_validation) { false }
+ # let(:errors) { [:title] }
+ # let(:enabled) { true }
+ # let(:type) { instance_double('Comments', public_submission_enabled: enabled) }
+ # let(:entry) { instance_double('Entry', title: 'Hello world', content_type: type, valid?: first_validation, errors: errors, attributes: { title: 'Hello world' }, localized_attributes: []) }
+ # let(:slug) { 'comments' }
+
+ # before do
+ # allow(type_repository).to receive(:by_slug).and_return(type)
+ # allow(type_repository).to receive(:look_for_unique_fields).and_return(unique_fields)
+ # allow(entry_repository).to receive(:build).with(attributes).and_return(entry)
+ # end
+
+ # context 'public submission disabled' do
+
+ # let(:enabled) { false }
+ # it { is_expected.to eq nil }
+
+ # end
+
+ # context 'valid' do
+
+ # before { expect(entry_repository).to receive(:create) }
+
+ # let(:first_validation) { true }
+ # let(:errors) { {} }
+
+ # it { is_expected.to eq entry }
+ # it { expect(subject.errors.empty?).to eq true }
+
+ # end
+
+ # context 'not valid' do
+
+ # before { expect(entry_repository).not_to receive(:create) }
+
+ # it { is_expected.to eq entry }
+ # it { expect(subject.errors).to eq([:title]) }
+
+ # context 'with unique fields' do
+
+ # let(:unique_fields) { { title: instance_double('Field', name: 'title') } }
+
+ # before do
+ # allow(entry_repository).to receive(:exists?).with(title: 'Hello world').and_return(true)
+ # expect(entry.errors).to receive(:add).with(:title, :unique).and_return(true)
+ # end
+
+ # it { is_expected.to eq entry }
+ # it { expect(subject.errors).to eq([:title]) }
+
+ # end
+
+ # end
+
+ # end
+
+ # end
+
+ # end
spec/unit/services/email_service_spec.rb +198 -0
@@ @@ -0,0 +1,198 @@
+ require 'spec_helper'
+
+ describe Locomotive::Steam::EmailService do
+
+ let(:page) { nil }
+ let(:page_finder) { instance_double('PageFinder', by_handle: page) }
+ let(:liquid_parser) { Locomotive::Steam::LiquidParserService.new(nil, nil) }
+ let(:asset_host) { instance_double('AssetHost') }
+ let(:simulation) { false }
+ let(:service) { described_class.new(page_finder, liquid_parser, asset_host, simulation) }
+
+ # uncomment the line below for DEBUG purpose
+ before { allow(service.logger).to receive(:info).and_return(true) }
+
+ describe '#send' do
+
+ let(:smtp_options) { { address: 'smtp.example.com', user_name: 'user', password: 'password' } }
+ let(:options) { { to: 'john@doe.net', from: 'me@locomotivecms.com', subject: 'Hello world', body: 'Hello {{ to }}', smtp: smtp_options, html: false } }
+ let(:context) { ::Liquid::Context.new({ 'name' => 'John', 'to' => 'john@doe.net' }, {}, {}) }
+
+ subject { service.send_email(options, context) }
+
+ it 'sends the email over Pony' do
+ expect(Pony).to receive(:mail).with(
+ to: 'john@doe.net',
+ from: 'me@locomotivecms.com',
+ subject: 'Hello world',
+ body: 'Hello john@doe.net',
+ via: :smtp,
+ via_options: {
+ address: 'smtp.example.com',
+ user_name: 'user',
+ password: 'password'
+ }
+ )
+ subject
+ end
+
+ context 'simulation mode' do
+
+ let(:simulation) { true }
+
+ it "doesn't call Pony.mail" do
+ expect(Pony).not_to receive(:mail)
+ subject
+ end
+
+ end
+
+ describe 'no body, no page handle' do
+
+ let(:options) { { to: 'john@doe.net', from: 'me@locomotivecms.com', subject: 'Hello world', smtp: smtp_options, html: false } }
+
+ it { expect { subject }.to raise_error('[EmailService] the body or page_handle options are missing.')}
+
+ end
+
+ describe 'use a page as the body of the email' do
+
+ let(:page) { instance_double('Page', liquid_source: '<html><body><h1>Hello {{ name }}</h1></body></html>') }
+ let(:options) { { to: 'john@doe.net', from: 'me@locomotivecms.com', subject: 'Hello world', page_handle: 'notification-email', smtp: smtp_options, html: true } }
+
+ it 'sends the email over Pony' do
+ expect(Pony).to receive(:mail).with(
+ to: 'john@doe.net',
+ from: 'me@locomotivecms.com',
+ subject: 'Hello world',
+ html_body: '<html><body><h1>Hello John</h1></body></html>',
+ via: :smtp,
+ via_options: {
+ address: 'smtp.example.com',
+ user_name: 'user',
+ password: 'password'
+ }
+ )
+ subject
+ end
+
+ context "the page doesn't exist" do
+
+ let(:page) { nil }
+
+ it { expect { subject }.to raise_error('[EmailService] No page found with the following handle: notification-email') }
+
+ end
+
+ end
+
+ describe 'with attachments' do
+
+ let(:options) { { to: 'john@doe.net', from: 'me@locomotivecms.com', subject: 'Hello world', body: 'Hello {{ to }}', smtp: smtp_options, attachments: attachments, html: false } }
+
+ context 'local attachment' do
+
+ let(:attachments) { { 'foo.txt' => '/local/foo.txt' } }
+
+ before do
+ expect(asset_host).to receive(:compute).with('/local/foo.txt', false).and_return('http://acme.org/local/foo.txt')
+ expect(Net::HTTP).to receive(:get).with(URI('http://acme.org/local/foo.txt')).and_return('Foo')
+ end
+
+ it 'sends the email over Pony' do
+ expect(Pony).to receive(:mail).with(
+ to: 'john@doe.net',
+ from: 'me@locomotivecms.com',
+ subject: 'Hello world',
+ body: 'Hello john@doe.net',
+ attachments: { 'foo.txt' => 'Foo' },
+ via: :smtp,
+ via_options: {
+ address: 'smtp.example.com',
+ user_name: 'user',
+ password: 'password'
+ }
+ )
+ subject
+ end
+
+ end
+
+ context 'remote attachment' do
+
+ let(:attachments) { { 'bar.txt' => 'http://acme.org/bar.txt' } }
+
+ it 'sends the email over Pony' do
+ expect(Net::HTTP).to receive(:get).with(URI('http://acme.org/bar.txt')).and_return('Bar')
+ expect(Pony).to receive(:mail).with(
+ to: 'john@doe.net',
+ from: 'me@locomotivecms.com',
+ subject: 'Hello world',
+ body: 'Hello john@doe.net',
+ attachments: { 'bar.txt' => 'Bar' },
+ via: :smtp,
+ via_options: {
+ address: 'smtp.example.com',
+ user_name: 'user',
+ password: 'password'
+ }
+ )
+ subject
+ end
+
+ context 'attachment not found' do
+
+ it "doesn't send the email" do
+ expect(Net::HTTP).to receive(:get).with(URI('http://acme.org/bar.txt')).and_raise('URL not responding')
+ expect(Pony).to receive(:mail).with(
+ to: 'john@doe.net',
+ from: 'me@locomotivecms.com',
+ subject: 'Hello world',
+ body: 'Hello john@doe.net',
+ attachments: { 'bar.txt' => nil },
+ via: :smtp,
+ via_options: {
+ address: 'smtp.example.com',
+ user_name: 'user',
+ password: 'password'
+ }
+ )
+ subject
+ end
+
+ end
+
+ end
+
+ context 'inline string' do
+
+ let(:attachments) { { 'bar.txt' => 'Bar' } }
+
+ it 'sends the email over Pony' do
+ expect(Pony).to receive(:mail).with(
+ to: 'john@doe.net',
+ from: 'me@locomotivecms.com',
+ subject: 'Hello world',
+ body: 'Hello john@doe.net',
+ attachments: { 'bar.txt' => 'Bar' },
+ via: :smtp,
+ via_options: {
+ address: 'smtp.example.com',
+ user_name: 'user',
+ password: 'password'
+ }
+ )
+ subject
+ end
+
+ end
+
+ end
+
+ end
+
+ def default_options
+
+ end
+
+ end
spec/unit/services/entry_submission_service_spec.rb +151 -86
@@ @@ -2,38 +2,47 @@ require 'spec_helper'
describe Locomotive::Steam::EntrySubmissionService do
- let(:site) { instance_double('Site', default_locale: 'en') }
- let(:locale) { 'en' }
- let(:type_repository) { instance_double('ContentTypeRepository') }
- let(:entry_repository) { instance_double('Repository', site: site, locale: locale, content_type_repository: type_repository) }
- let(:service) { described_class.new(type_repository, entry_repository, locale) }
-
- before { allow(entry_repository).to receive(:with).and_return(entry_repository) }
+ let(:entry_service) { instance_double('ContentEntryService') }
+ let(:service) { described_class.new(entry_service) }
describe '#find' do
- let(:type_slug) { 'articles' }
- let(:slug) { 'hello-world' }
- subject { service.find(type_slug, slug) }
+ subject { service.find('messages', '42') }
+
+ it { expect(entry_service).to receive(:find).with('messages', '42'); subject }
+
+ end
+
+ describe '#submit' do
+
+ let(:content_type) { instance_double('ContentType', public_submission_enabled: public_submission_enabled) }
- context 'unknown content type' do
+ before { allow(entry_service).to receive(:get_type).with('messages').and_return(content_type) }
- before { allow(type_repository).to receive(:by_slug).and_return(nil) }
+ subject { service.submit('messages', { name: 'John Doe', body: 'Lorem ipsum' }) }
+
+ context "the content type doesn't exist" do
+
+ let(:public_submission_enabled) { true }
+ let(:content_type) { nil }
it { is_expected.to eq nil }
end
- context 'existing content type' do
+ context "the content type exists but it's not enabled for public submission" do
- let(:type) { instance_double('Articles') }
- let(:entry) { instance_double('Entry', title: 'Hello world', content_type: type, attributes: { title: 'Hello world' }, localized_attributes: []) }
+ let(:public_submission_enabled) { false }
+ it { is_expected.to eq nil }
- before do
- allow(type_repository).to receive(:by_slug).and_return(type)
- allow(entry_repository).to receive(:by_slug).with('hello-world').and_return(entry)
- end
+ end
+
+ context 'the content type exists and is enabled for public submission' do
- it { is_expected.to eq entry }
+ let(:public_submission_enabled) { true }
+ it 'calls the entry service to create the message' do
+ expect(entry_service).to receive(:create).with(content_type, { name: 'John Doe', body: 'Lorem ipsum' })
+ subject
+ end
end
@@ @@ -41,112 +50,168 @@ describe Locomotive::Steam::EntrySubmissionService do
describe '#to_json' do
- let(:entry) { nil }
+ let(:entry) { instance_double('Entry', to_json: "{'name':'John'}") }
+
subject { service.to_json(entry) }
- it { is_expected.to eq nil }
+ it { is_expected.to eq("{'name':'John'}") }
- context 'existing content entry' do
+ context 'entry is nil' do
- let(:field) { instance_double('TitleField', name: :title, type: :string) }
- let(:fields) { [field] }
- let(:type) { instance_double('Articles', slug: 'articles', label_field_name: :title, fields_by_name: { title: field }, persisted_field_names: [:title]) }
- let(:entry) { Locomotive::Steam::ContentEntry.new(_slug: 'hello-world', title: 'Hello world', content_type: type) }
+ let(:entry) { nil }
+ it { is_expected.to eq nil }
- before { allow(type).to receive(:fields).and_return(instance_double('FieldRepository', all: fields)) }
+ end
- it { is_expected.to match %r{{"_id":null,"_slug":"hello-world","_label":"Hello world","_visible":true,"_position":0,"content_type_slug":"articles","created_at":"[^\"]+","updated_at":"[^\"]+","title":"Hello world"}} }
+ end
- context 'with errors' do
+ end
- before { entry.errors.add(:title, "can't be blank") }
+ # let(:site) { instance_double('Site', default_locale: 'en') }
+ # let(:locale) { 'en' }
+ # let(:type_repository) { instance_double('ContentTypeRepository') }
+ # let(:entry_repository) { instance_double('Repository', site: site, locale: locale, content_type_repository: type_repository) }
+ # let(:service) { described_class.new(type_repository, entry_repository, locale) }
- it { is_expected.to match %r{,\"errors\":\{\"title\":\[\"can't be blank\"\]\}} }
+ # before { allow(entry_repository).to receive(:with).and_return(entry_repository) }
- end
+ # describe '#find' do
- end
+ # let(:type_slug) { 'articles' }
+ # let(:slug) { 'hello-world' }
+ # subject { service.find(type_slug, slug) }
- end
+ # context 'unknown content type' do
- describe '#submit' do
+ # before { allow(type_repository).to receive(:by_slug).and_return(nil) }
+ # it { is_expected.to eq nil }
- let(:slug) { nil }
- let(:attributes) { { title: 'Hello world' } }
- subject { service.submit(slug, attributes) }
+ # end
- it { is_expected.to eq nil }
+ # context 'existing content type' do
- context 'unknown content type' do
+ # let(:type) { instance_double('Articles') }
+ # let(:entry) { instance_double('Entry', title: 'Hello world', content_type: type, attributes: { title: 'Hello world' }, localized_attributes: []) }
- let(:slug) { 'articles' }
+ # before do
+ # allow(type_repository).to receive(:by_slug).and_return(type)
+ # allow(entry_repository).to receive(:by_slug).with('hello-world').and_return(entry)
+ # end
- before { allow(type_repository).to receive(:by_slug).with('articles').and_return nil }
+ # it { is_expected.to eq entry }
- it { is_expected.to eq nil }
+ # end
- end
+ # end
- context 'existing content type' do
+ # describe '#to_json' do
- let(:unique_fields) { {} }
- let(:first_validation) { false }
- let(:errors) { [:title] }
- let(:enabled) { true }
- let(:type) { instance_double('Comments', public_submission_enabled: enabled) }
- let(:entry) { instance_double('Entry', title: 'Hello world', content_type: type, valid?: first_validation, errors: errors, attributes: { title: 'Hello world' }, localized_attributes: []) }
- let(:slug) { 'comments' }
+ # let(:entry) { nil }
+ # subject { service.to_json(entry) }
- before do
- allow(type_repository).to receive(:by_slug).and_return(type)
- allow(type_repository).to receive(:look_for_unique_fields).and_return(unique_fields)
- allow(entry_repository).to receive(:build).with(attributes).and_return(entry)
- end
+ # it { is_expected.to eq nil }
- context 'public submission disabled' do
+ # context 'existing content entry' do
- let(:enabled) { false }
- it { is_expected.to eq nil }
+ # let(:field) { instance_double('TitleField', name: :title, type: :string) }
+ # let(:fields) { [field] }
+ # let(:type) { instance_double('Articles', slug: 'articles', label_field_name: :title, fields_by_name: { title: field }, persisted_field_names: [:title]) }
+ # let(:entry) { Locomotive::Steam::ContentEntry.new(_slug: 'hello-world', title: 'Hello world', content_type: type) }
- end
+ # before { allow(type).to receive(:fields).and_return(instance_double('FieldRepository', all: fields)) }
- context 'valid' do
+ # it { is_expected.to match %r{{"_id":null,"_slug":"hello-world","_label":"Hello world","_visible":true,"_position":0,"content_type_slug":"articles","created_at":"[^\"]+","updated_at":"[^\"]+","title":"Hello world"}} }
- before { expect(entry_repository).to receive(:create) }
+ # context 'with errors' do
- let(:first_validation) { true }
- let(:errors) { {} }
+ # before { entry.errors.add(:title, "can't be blank") }
- it { is_expected.to eq entry }
- it { expect(subject.errors.empty?).to eq true }
+ # it { is_expected.to match %r{,\"errors\":\{\"title\":\[\"can't be blank\"\]\}} }
- end
+ # end
- context 'not valid' do
+ # end
- before { expect(entry_repository).not_to receive(:create) }
+ # end
- it { is_expected.to eq entry }
- it { expect(subject.errors).to eq([:title]) }
+ # describe '#submit' do
- context 'with unique fields' do
+ # let(:slug) { nil }
+ # let(:attributes) { { title: 'Hello world' } }
+ # subject { service.submit(slug, attributes) }
- let(:unique_fields) { { title: instance_double('Field', name: 'title') } }
+ # it { is_expected.to eq nil }
- before do
- allow(entry_repository).to receive(:exists?).with(title: 'Hello world').and_return(true)
- expect(entry.errors).to receive(:add).with(:title, :unique).and_return(true)
- end
+ # context 'unknown content type' do
- it { is_expected.to eq entry }
- it { expect(subject.errors).to eq([:title]) }
+ # let(:slug) { 'articles' }
- end
+ # before { allow(type_repository).to receive(:by_slug).with('articles').and_return nil }
- end
+ # it { is_expected.to eq nil }
- end
+ # end
- end
+ # context 'existing content type' do
- end
+ # let(:unique_fields) { {} }
+ # let(:first_validation) { false }
+ # let(:errors) { [:title] }
+ # let(:enabled) { true }
+ # let(:type) { instance_double('Comments', public_submission_enabled: enabled) }
+ # let(:entry) { instance_double('Entry', title: 'Hello world', content_type: type, valid?: first_validation, errors: errors, attributes: { title: 'Hello world' }, localized_attributes: []) }
+ # let(:slug) { 'comments' }
+
+ # before do
+ # allow(type_repository).to receive(:by_slug).and_return(type)
+ # allow(type_repository).to receive(:look_for_unique_fields).and_return(unique_fields)
+ # allow(entry_repository).to receive(:build).with(attributes).and_return(entry)
+ # end
+
+ # context 'public submission disabled' do
+
+ # let(:enabled) { false }
+ # it { is_expected.to eq nil }
+
+ # end
+
+ # context 'valid' do
+
+ # before { expect(entry_repository).to receive(:create) }
+
+ # let(:first_validation) { true }
+ # let(:errors) { {} }
+
+ # it { is_expected.to eq entry }
+ # it { expect(subject.errors.empty?).to eq true }
+
+ # end
+
+ # context 'not valid' do
+
+ # before { expect(entry_repository).not_to receive(:create) }
+
+ # it { is_expected.to eq entry }
+ # it { expect(subject.errors).to eq([:title]) }
+
+ # context 'with unique fields' do
+
+ # let(:unique_fields) { { title: instance_double('Field', name: 'title') } }
+
+ # before do
+ # allow(entry_repository).to receive(:exists?).with(title: 'Hello world').and_return(true)
+ # expect(entry.errors).to receive(:add).with(:title, :unique).and_return(true)
+ # end
+
+ # it { is_expected.to eq entry }
+ # it { expect(subject.errors).to eq([:title]) }
+
+ # end
+
+ # end
+
+ # end
+
+ # end
+
+ # end