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 |