fix all broken specs + fix a small bug when comparing _auth_reset_sent_at to the current time

did committed Nov 15, 2016
commit 92d09d396cacd2e5f20fb58b23e0b397f2f968dd
Showing 50 changed files with 355 additions and 92 deletions
CHANGELOG.md +0 -29
@@ @@ -1,29 +0,0 @@
- ### VERSION 0.1.2
-
- * enhancements
- * Change Markdown library from redcarpet to kramdown for support JRuby
-
- ### VERSION 0.1.1
-
- * refactoring
- * remove logger from core
-
- ### VERSION 0.0.1
-
- * bug fix
- *
-
- * refactoring
- *
-
- * enhancements
- *
-
- * backwards incompatible changes
- *
-
- * deprecations
- *
-
- * todos
- *
\ No newline at end of file
Gemfile.lock +0 -3
@@ @@ -26,7 +26,6 @@ PATH
sanitize (~> 4.0.1)
sass (~> 3.4.21)
sprockets (~> 3.5.2)
- warden (~> 1.2.6)
GEM
remote: https://rubygems.org/
@@ @@ -189,8 +188,6 @@ GEM
tins (1.12.0)
tzinfo (1.2.2)
thread_safe (~> 0.1)
- warden (1.2.6)
- rack (>= 1.0)
yui-compressor (0.12.0)
PLATFORMS
locomotive/steam/adapters/memory/condition.rb b/lib/locomotive/steam/adapters/memory/condition.rb +1 -1
@@ @@ -47,7 +47,7 @@ module Locomotive::Steam
protected
def entry_value(entry)
- value = entry.send(@field)
+ value = entry.send(@field) rescue nil # not just it's safe to rely on rescue (hidden errors)
if value.respond_to?(:translations)
value[@locale]
locomotive/steam/entities/content_type_field.rb b/lib/locomotive/steam/entities/content_type_field.rb +1 -0
@@ @@ -66,6 +66,7 @@ module Locomotive::Steam
when :belongs_to, :select then "#{name}_id"
when :many_to_many then "#{name.singularize}_ids"
when :has_many then nil
+ when :password then nil
else name
end
end
locomotive/steam/middlewares/auth.rb b/lib/locomotive/steam/middlewares/auth.rb +17 -0
@@ @@ -55,6 +55,19 @@ module Locomotive::Steam
append_message(status)
end
+ def reset_password(options)
+ return if authenticated?
+
+ status, entry = services.auth.reset_password(options)
+
+ if status == :password_reset
+ store_authenticated(entry)
+ redirect_to options.callback || mounted_on
+ end
+
+ append_message(status)
+ end
+
def load_authenticated_entry
entry_type = request.session[:authenticated_entry_type]
entry_id = request.session[:authenticated_entry_id]
@@ @@ -129,6 +142,10 @@ module Locomotive::Steam
params[:auth_reset_password_url]
end
+ def reset_token
+ params[:auth_reset_token]
+ end
+
def from
params[:auth_email_from] || 'support@locomotivecms.com'
end
locomotive/steam/services.rb b/lib/locomotive/steam/services.rb +1 -1
@@ @@ -79,7 +79,7 @@ module Locomotive
end
register :url_builder do
- Steam::UrlBuilderService.new(current_site, locale, request, page_finder)
+ Steam::UrlBuilderService.new(current_site, locale, request)
end
register :theme_asset_url do
locomotive/steam/services/auth_service.rb b/lib/locomotive/steam/services/auth_service.rb +29 -2
@@ @@ -3,6 +3,9 @@ module Locomotive
class AuthService
+ MIN_PASSWORD_LENGTH = 6
+ RESET_TOKEN_LIFETIME = 6 * 3600 # 6 hours in seconds
+
attr_accessor_initialize :entries, :email_service
def find_authenticated_resource(type, id)
@@ @@ -32,10 +35,10 @@ module Locomotive
else
entries.update_decorated_entry(entry, {
'_auth_reset_token' => SecureRandom.hex,
- '_auth_reset_sent_at' => Time.zone.now
+ '_auth_reset_sent_at' => Time.zone.now.iso8601
})
- context['reset_password_url'] = options.reset_password_url + '?token=' + entry['_auth_reset_token']
+ context['reset_password_url'] = options.reset_password_url + '?auth_reset_token=' + entry['_auth_reset_token']
context[options.type.singularize] = entry
send_reset_password_instructions(options, context)
@@ @@ -44,6 +47,30 @@ module Locomotive
end
end
+ def reset_password(options)
+ return :invalid_token if options.reset_token.blank?
+ return :password_too_short if options.password.to_s.size < MIN_PASSWORD_LENGTH
+
+ entry = entries.all(options.type, '_auth_reset_token' => options.reset_token).first
+
+ if entry
+ sent_at = Time.parse(entry[:_auth_reset_sent_at]).to_i
+ now = Time.zone.now.to_i - RESET_TOKEN_LIFETIME
+
+ if sent_at >= now
+ entries.update_decorated_entry(entry, {
+ "#{options.password_field}_hash" => BCrypt::Password.create(options.password),
+ '_auth_reset_token' => nil,
+ '_auth_reset_sent_at' => nil
+ })
+
+ return [:password_reset, entry]
+ end
+ end
+
+ :invalid_token
+ end
+
private
def send_reset_password_instructions(options, context)
locomotive/steam/services/content_entry_service.rb b/lib/locomotive/steam/services/content_entry_service.rb +0 -2
@@ @@ -60,8 +60,6 @@ module Locomotive
with_repository(decorated_entry.content_type) do |_repository|
entry = decorated_entry.__getobj__
- puts clean_attributes(attributes).inspect
-
entry.change(clean_attributes(attributes))
_repository.update(entry)
locomotive/steam/services/url_builder_service.rb b/lib/locomotive/steam/services/url_builder_service.rb +1 -7
@@ @@ -3,15 +3,9 @@ module Locomotive
class UrlBuilderService
- attr_accessor_initialize :site, :current_locale, :request, :page_finder
-
- def absolute_url_for(page, locale = nil)
- request.base_url + url_for(page, locale)
- end
+ attr_accessor_initialize :site, :current_locale, :request
def url_for(page, locale = nil)
- page = page_finder.by_handle(page) if page.is_a?(String)
-
prefix(_url_for(page, locale))
end
locomotivecms_steam.gemspec +0 -1
@@ @@ -33,7 +33,6 @@ Gem::Specification.new do |spec|
spec.add_dependency 'dragonfly', '~> 1.0.12'
spec.add_dependency 'moneta', '~> 0.8.0'
spec.add_dependency 'rack_csrf', '~> 2.5.0'
- spec.add_dependency 'warden', '~> 1.2.6'
spec.add_dependency 'sprockets', '~> 3.5.2'
spec.add_dependency 'sass', '~> 3.4.21'
spec/fixtures/default/app/content_types/songs.yml +2 -1
@@ @@ -15,6 +15,7 @@ fields:
label: Cover
type: file
required: true
+ # localized: true # required when pushing the site with Wagon
- short_description:
type: text
text_formatting: html
@@ @@ -22,4 +23,4 @@ fields:
type: string
hint: Url to a service like Blip for instance
- duration:
- hint: "format like: mm:ss"
\ No newline at end of file
+ hint: "format like: mm:ss"
spec/fixtures/default/app/views/pages/account/reset_password.liquid +42 -1
@@ @@ -1 +1,42 @@
- reset_password.liquid
+ ---
+ title: Change your password
+ published: true
+ listed: false
+ handle: reset_password
+ ---
+ {% extends 'index' %}
+
+ {% block content %}
+
+ <h2>Change your password</h2>
+
+ {% if current_account %}
+ You're already authenticated!
+ {% else %}
+ <form action="{% path_to 'reset_password' %}" method="POST">
+ <input type="hidden" name="auth_action" value="reset_password" />
+ <input type="hidden" name="auth_content_type" value="accounts" />
+ <input type="hidden" name="auth_password_field" value="email" />
+ <input type="hidden" name="auth_reset_token" value="{{ params.token }}" />
+ <input type="hidden" name="auth_callback" value="{% path_to me %}" />
+
+ {% if auth_invalid_token %}
+ {{ auth_invalid_token | translate }}
+ {% endif %}
+
+ {% if auth_password_too_short %}
+ {{ auth_password_too_short | translate }}
+ {% endif %}
+
+ <div class="form-group">
+ <label for="auth-password">Your new password</label>
+ <input type="password" class="form-control" id="auth-password" placeholder="Password" name="auth_password">
+ </div>
+
+ <button type="submit" class="btn btn-default">Submit</button>
+ </form>
+
+ {% endif %}
+ </div>
+
+ {% endblock %}
spec/fixtures/default/config/translations.yml +8 -0
@@ @@ -17,3 +17,11 @@ auth_wrong_email:
auth_reset_password_instructions_sent:
en: "The instructions for changing your password have been emailed to you"
fr: "Les instructions pour modifier votre mot de passe viennent de vous être envoyées"
+
+ auth_invalid_token:
+ en: "The reset token is not valid anymore"
+ fr: "Le token de re-initialisation du mot de passe n'est plus valide"
+
+ auth_password_too_short:
+ en: "Your password is too short"
+ fr: "Votre mot de passe est trop court"
spec/fixtures/default/data/accounts.yml +12 -0
@@ @@ -1,3 +1,15 @@
- "John":
email: 'john@doe.net'
password: 'easyone'
+
+ - "Jane":
+ email: 'john@doe.net'
+ password: 'anotherone'
+ _auth_reset_token: '420000000000000'
+ _auth_reset_sent_at: '2020-01-01T15:00:00.000Z'
+
+ - "Mickey":
+ email: 'mickey@doe.net'
+ password: 'anotherone'
+ _auth_reset_token: '420000000000001'
+ _auth_reset_sent_at: '2016-01-01T15:00:00.000Z'
locomotive_accounts.bson b/spec/fixtures/mongodb/locomotive_accounts.bson +0 -0
locomotive_accounts.metadata.json b/spec/fixtures/mongodb/locomotive_accounts.metadata.json +1 -1
@@ @@ -1 +1 @@
- { "indexes" : [ { "v" : 1, "key" : { "_id" : 1 }, "name" : "_id_", "ns" : "locomotive_engine_wagon_dev.locomotive_accounts" } ] }
\ No newline at end of file
+ {"options":{},"indexes":[{"v":1,"key":{"_id":1},"name":"_id_","ns":"locomotive_engine_dev.locomotive_accounts"}]}
\ No newline at end of file
locomotive_activities.bson b/spec/fixtures/mongodb/locomotive_activities.bson +0 -0
locomotive_activities.metadata.json b/spec/fixtures/mongodb/locomotive_activities.metadata.json +1 -1
@@ @@ -1 +1 @@
- { "indexes" : [ { "v" : 1, "key" : { "_id" : 1 }, "name" : "_id_", "ns" : "locomotive_engine_wagon_dev.locomotive_activities" } ] }
\ No newline at end of file
+ {"options":{},"indexes":[{"v":1,"key":{"_id":1},"name":"_id_","ns":"locomotive_engine_dev.locomotive_activities"}]}
\ No newline at end of file
locomotive_content_assets.bson b/spec/fixtures/mongodb/locomotive_content_assets.bson +0 -0
locomotive_content_assets.metadata.json b/spec/fixtures/mongodb/locomotive_content_assets.metadata.json +1 -1
@@ @@ -1 +1 @@
- { "indexes" : [ { "v" : 1, "key" : { "_id" : 1 }, "name" : "_id_", "ns" : "locomotive_engine_wagon_dev.locomotive_content_assets" } ] }
\ No newline at end of file
+ {"options":{},"indexes":[{"v":1,"key":{"_id":1},"name":"_id_","ns":"locomotive_engine_dev.locomotive_content_assets"}]}
\ No newline at end of file
locomotive_content_entries.bson b/spec/fixtures/mongodb/locomotive_content_entries.bson +0 -0
locomotive_content_entries.metadata.json b/spec/fixtures/mongodb/locomotive_content_entries.metadata.json +1 -1
@@ @@ -1 +1 @@
- { "indexes" : [ { "v" : 1, "key" : { "_id" : 1 }, "name" : "_id_", "ns" : "locomotive_engine_wagon_dev.locomotive_content_entries" } ] }
\ No newline at end of file
+ {"options":{},"indexes":[{"v":1,"key":{"_id":1},"name":"_id_","ns":"locomotive_engine_dev.locomotive_content_entries"}]}
\ No newline at end of file
locomotive_content_types.bson b/spec/fixtures/mongodb/locomotive_content_types.bson +0 -0
locomotive_content_types.metadata.json b/spec/fixtures/mongodb/locomotive_content_types.metadata.json +1 -1
@@ @@ -1 +1 @@
- { "indexes" : [ { "v" : 1, "key" : { "_id" : 1 }, "name" : "_id_", "ns" : "locomotive_engine_wagon_dev.locomotive_content_types" } ] }
\ No newline at end of file
+ {"options":{},"indexes":[{"v":1,"key":{"_id":1},"name":"_id_","ns":"locomotive_engine_dev.locomotive_content_types"}]}
\ No newline at end of file
locomotive_pages.bson b/spec/fixtures/mongodb/locomotive_pages.bson +0 -0
locomotive_pages.metadata.json b/spec/fixtures/mongodb/locomotive_pages.metadata.json +1 -1
@@ @@ -1 +1 @@
- { "indexes" : [ { "v" : 1, "key" : { "_id" : 1 }, "name" : "_id_", "ns" : "locomotive_engine_wagon_dev.locomotive_pages" } ] }
\ No newline at end of file
+ {"options":{},"indexes":[{"v":1,"key":{"_id":1},"name":"_id_","ns":"locomotive_engine_dev.locomotive_pages"}]}
\ No newline at end of file
locomotive_sites.bson b/spec/fixtures/mongodb/locomotive_sites.bson +0 -0
locomotive_sites.metadata.json b/spec/fixtures/mongodb/locomotive_sites.metadata.json +1 -1
@@ @@ -1 +1 @@
- { "indexes" : [ { "v" : 1, "key" : { "_id" : 1 }, "name" : "_id_", "ns" : "locomotive_engine_wagon_dev.locomotive_sites" } ] }
\ No newline at end of file
+ {"options":{},"indexes":[{"v":1,"key":{"_id":1},"name":"_id_","ns":"locomotive_engine_dev.locomotive_sites"}]}
\ No newline at end of file
locomotive_snippets.bson b/spec/fixtures/mongodb/locomotive_snippets.bson +0 -0
locomotive_snippets.metadata.json b/spec/fixtures/mongodb/locomotive_snippets.metadata.json +1 -1
@@ @@ -1 +1 @@
- { "indexes" : [ { "v" : 1, "key" : { "_id" : 1 }, "name" : "_id_", "ns" : "locomotive_engine_wagon_dev.locomotive_snippets" } ] }
\ No newline at end of file
+ {"options":{},"indexes":[{"v":1,"key":{"_id":1},"name":"_id_","ns":"locomotive_engine_dev.locomotive_snippets"}]}
\ No newline at end of file
locomotive_theme_assets.bson b/spec/fixtures/mongodb/locomotive_theme_assets.bson +0 -0
locomotive_theme_assets.metadata.json b/spec/fixtures/mongodb/locomotive_theme_assets.metadata.json +1 -1
@@ @@ -1 +1 @@
- { "indexes" : [ { "v" : 1, "key" : { "_id" : 1 }, "name" : "_id_", "ns" : "locomotive_engine_wagon_dev.locomotive_theme_assets" } ] }
\ No newline at end of file
+ {"options":{},"indexes":[{"v":1,"key":{"_id":1},"name":"_id_","ns":"locomotive_engine_dev.locomotive_theme_assets"}]}
\ No newline at end of file
locomotive_translations.bson b/spec/fixtures/mongodb/locomotive_translations.bson +0 -0
locomotive_translations.metadata.json b/spec/fixtures/mongodb/locomotive_translations.metadata.json +1 -1
@@ @@ -1 +1 @@
- { "indexes" : [ { "v" : 1, "key" : { "_id" : 1 }, "name" : "_id_", "ns" : "locomotive_engine_wagon_dev.locomotive_translations" } ] }
\ No newline at end of file
+ {"options":{},"indexes":[{"v":1,"key":{"_id":1},"name":"_id_","ns":"locomotive_engine_dev.locomotive_translations"}]}
\ No newline at end of file
system.indexes.bson b/spec/fixtures/mongodb/system.indexes.bson +0 -0
spec/integration/repositories/content_entry_repository_spec.rb +1 -1
@@ @@ -116,7 +116,7 @@ describe Locomotive::Steam::ContentEntryRepository 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') }
+ let(:entry_id) { BSON::ObjectId.from_string('5829ffa087f6435971756881') }
end
spec/integration/repositories/content_type_repository_spec.rb +1 -1
@@ @@ -13,7 +13,7 @@ describe Locomotive::Steam::ContentTypeRepository do
describe '#all' do
subject { repository.all }
- it { expect(subject.size).to eq 5 }
+ it { expect(subject.size).to eq 6 }
end
describe '#by_slug' do
spec/integration/repositories/page_repository_spec.rb +3 -3
@@ @@ -14,7 +14,7 @@ describe Locomotive::Steam::PageRepository do
describe '#all' do
let(:conditions) { {} }
subject { repository.all(conditions) }
- it { expect(subject.size).to eq 26 }
+ it { expect(subject.size).to eq 33 }
context 'with conditions' do
let(:conditions) { { fullpath: 'index', 'slug.ne' => '404' } }
@@ @@ -34,7 +34,7 @@ describe Locomotive::Steam::PageRepository do
describe '#only_handle_and_fullpath' do
subject { repository.only_handle_and_fullpath }
- it { expect(subject.size).to eq 3 }
+ it { expect(subject.size).to eq 8 }
end
describe '#by_fullpath' do
@@ @@ -78,7 +78,7 @@ describe Locomotive::Steam::PageRepository do
describe '#children_of' do
let(:page) { repository.root }
subject { repository.children_of(page) }
- it { expect(subject.size).to eq 15 }
+ it { expect(subject.size).to eq 17 }
end
describe '#editable_element_for' do
spec/integration/repositories/theme_asset_repository_spec.rb +1 -1
@@ @@ -26,7 +26,7 @@ describe Locomotive::Steam::ThemeAssetRepository do
describe '#checksums' do
subject { repository.checksums }
it { expect(subject.size).to eq 16 }
- it { expect(subject['stylesheets/application.css']).to eq '3bacf4c2b7877e230e6990d72dae7724' }
+ it { expect(subject['stylesheets/application.css']).to eq 'f431407c21db339b7759c2d7ded2553f' }
end
end
spec/integration/repositories/translation_repository_spec.rb +1 -1
@@ @@ -13,7 +13,7 @@ describe Locomotive::Steam::TranslationRepository do
describe '#all' do
subject { repository.all }
- it { expect(subject.size).to eq 1 }
+ it { expect(subject.size).to eq 7 }
end
describe '#by_key' do
spec/integration/server/auth_spec.rb +62 -6
@@ @@ -109,7 +109,7 @@ describe 'Authentication' do
} }
it 'renders the forgot password page with an error message' do
- forgot_password
+ post '/account/forgot-password', params
expect(last_response.status).to eq 200
expect(last_response.body).to include 'Forgot your password'
expect(last_response.body).to include 'Your email is unknown'
@@ @@ -120,17 +120,73 @@ describe 'Authentication' do
let(:email) { 'john@doe.net' }
it 'sends an email to the account' do
- forgot_password
+ post '/account/forgot-password', params
expect(last_response.status).to eq 200
expect(last_response.body).to include "The instructions for changing your password have been emailed to you"
end
end
- def forgot_password(follow_redirect = false)
- post '/account/forgot-password', params
- follow_redirect! if follow_redirect
- last_response
+ end
+
+ describe 'reset password action' do
+
+ let(:token) { '' }
+ let(:new_password) { 'newone!' }
+
+ let(:params) { {
+ auth_action: 'reset_password',
+ auth_content_type: 'accounts',
+ auth_password_field: 'password',
+ auth_password: new_password,
+ auth_reset_token: token,
+ auth_callback: '/account/me'
+ } }
+
+ it 'renders the reset password page with an error message' do
+ post '/account/reset-password', params
+ expect(last_response.status).to eq 200
+ expect(last_response.body).to include 'Change your password'
+ expect(last_response.body).to include 'The reset token is not valid anymore'
+ end
+
+ context 'with an expired token' do
+
+ let(:token) { '420000000000001' }
+
+ it 'renders the reset password page with an error message' do
+ post '/account/reset-password', params
+ expect(last_response.status).to eq 200
+ expect(last_response.body).to include 'Change your password'
+ expect(last_response.body).to include 'The reset token is not valid anymore'
+ end
+
+ end
+
+ context 'with a valid token' do
+
+ let(:token) { '420000000000000' }
+
+ it 'sends an email to the account' do
+ post '/account/reset-password', params
+ expect(last_response.status).to eq 301
+ follow_redirect!
+ expect(last_response.body).to include "My name is Jane and I'm logged in!"
+ end
+
+ context 'with a too short password' do
+
+ let(:new_password) { 'short' }
+
+ it 'renders the reset password page with an error message' do
+ post '/account/reset-password', params
+ expect(last_response.status).to eq 200
+ expect(last_response.body).to include 'Change your password'
+ expect(last_response.body).to include 'Your password is too short'
+ end
+
+ end
+
end
end
spec/integration/server/nav_spec.rb +1 -1
@@ @@ -30,7 +30,7 @@ describe Locomotive::Steam::Server do
end
it 'lists all the pages from the site liquid drop' do
- is_expected.to include('<!-- TEST -->About Us - Music - Store - Contact Us - Events - Basic page - A sample contest - Various uses of the with_scope tag - Grunge leaders - Tags - Unlisted pages - Archives - All the pages - Layouts - Songs<!-- TEST -->')
+ is_expected.to include('<!-- TEST -->About Us - Music - Store - Contact Us - Events - Basic page - A sample contest - Various uses of the with_scope tag - Grunge leaders - Tags - Unlisted pages - Archives - Account - All the pages - Emails - Layouts - Songs<!-- TEST -->')
end
describe 'with wrapper' do
spec/integration/server/sitemap_spec.rb +1 -1
@@ @@ -21,7 +21,7 @@ describe Locomotive::Steam::Server do
it 'checks if it looks valid' do
expect(Nokogiri::XML(subject).errors.empty?).to eq true
- expect(subject.scan(/<url>/).size).to eq 40
+ expect(subject.scan(/<url>/).size).to eq 45
expect(subject).to match("<loc>http://example.org/songs/song-number-2/band</loc>")
expect(subject).to match((<<-EOF
<url>
spec/integration/services/content_entry_service_spec.rb +1 -1
@@ @@ -45,7 +45,7 @@ describe Locomotive::Steam::ContentEntryService 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') }
+ let(:entry_id) { BSON::ObjectId.from_string('5829ffa087f6435971756881') }
describe '#create' do
subject { service.create('messages', { name: 'John', email: 'john@doe.net', message: 'Hello world!' }) }
spec/support/helpers.rb +1 -1
@@ @@ -4,7 +4,7 @@ module Spec
module Helpers
def mongodb_site_id
- BSON::ObjectId.from_string('561030e287f6431555000006')
+ BSON::ObjectId.from_string('5829ff6487f64359474164a1')
end
def reset!
spec/unit/adapters/filesystem/yaml_loaders/content_entry_spec.rb +17 -5
@@ @@ -6,7 +6,7 @@ require_relative '../../../../../lib/locomotive/steam/adapters/filesystem/yaml_l
describe Locomotive::Steam::Adapters::Filesystem::YAMLLoaders::ContentEntry do
let(:site_path) { default_fixture_site_path }
- let(:content_type) { instance_double('Bands', _id: 42, slug: 'bands', association_fields: [], select_fields: [], file_fields: []) }
+ let(:content_type) { instance_double('Bands', _id: 42, slug: 'bands', association_fields: [], select_fields: [], file_fields: [], password_fields: []) }
let(:scope) { instance_double('Scope', locale: :en, context: { content_type: content_type }) }
let(:loader) { described_class.new(site_path) }
@@ @@ -23,7 +23,7 @@ describe Locomotive::Steam::Adapters::Filesystem::YAMLLoaders::ContentEntry do
context 'a content type with a belongs_to field' do
let(:field) { instance_double('Field', name: 'band', type: :belongs_to) }
- let(:content_type) { instance_double('Songs', slug: 'songs', association_fields: [field], select_fields: [], file_fields: []) }
+ let(:content_type) { instance_double('Songs', slug: 'songs', association_fields: [field], select_fields: [], file_fields: [], password_fields: []) }
it 'adds a new attribute for the foreign key' do
expect(subject.first[:band_id]).to eq 'pearl-jam'
@@ @@ -36,7 +36,7 @@ describe Locomotive::Steam::Adapters::Filesystem::YAMLLoaders::ContentEntry do
context 'a content type with a select field' do
let(:field) { instance_double('Field', name: 'kind', type: :select) }
- let(:content_type) { instance_double('Bands', slug: 'bands', select_fields: [field], association_fields: [], file_fields: []) }
+ let(:content_type) { instance_double('Bands', slug: 'bands', select_fields: [field], association_fields: [], file_fields: [], password_fields: []) }
it 'adds a new attribute for the foreign key' do
expect(subject.first[:kind_id]).to eq 'grunge'
@@ @@ -45,12 +45,24 @@ describe Locomotive::Steam::Adapters::Filesystem::YAMLLoaders::ContentEntry do
end
+ context 'a content type with a password field' do
+
+ let(:field) { instance_double('Field', name: 'password', type: :password) }
+ let(:content_type) { instance_double('Accounts', slug: 'accounts', select_fields: [], association_fields: [], file_fields: [], password_fields: [field]) }
+
+ it 'adds a new attribute for the hashed password' do
+ expect(subject.first[:password_hash]).not_to eq 'easyone'
+ expect(subject.first[:password]).to eq nil
+ end
+
+ end
+
context 'a content type with a localized field' do
let(:options_scope) { instance_double('Scope', :locale= => true) }
let(:options) { instance_double('SelectOptionsRepository', scope: options_scope) }
let(:field) { instance_double('Field', name: 'category', type: :select, localized: true, select_options: options) }
- let(:content_type) { instance_double('Updates', slug: 'updates', select_fields: [field], association_fields: [], file_fields: []) }
+ let(:content_type) { instance_double('Updates', slug: 'updates', select_fields: [field], association_fields: [], file_fields: [], password_fields: []) }
it 'adds a new localized attribute for the foreign key' do
option = instance_double('Option', _id: 'General')
@@ @@ -65,7 +77,7 @@ describe Locomotive::Steam::Adapters::Filesystem::YAMLLoaders::ContentEntry do
context 'a content type with a file field' do
let(:field) { instance_double('Field', name: 'cover', type: :file) }
- let(:content_type) { instance_double('Songs', slug: 'songs', select_fields: [], association_fields: [], file_fields: [field]) }
+ let(:content_type) { instance_double('Songs', slug: 'songs', select_fields: [], association_fields: [], file_fields: [field], password_fields: []) }
it 'stores the size of the file' do
expect(subject.first[:cover_size]).to eq('default' => 14768)
spec/unit/adapters/filesystem/yaml_loaders/content_type_spec.rb +4 -4
@@ @@ -15,10 +15,10 @@ describe Locomotive::Steam::Adapters::Filesystem::YAMLLoaders::ContentType do
subject { loader.load(scope).sort { |a, b| a[:slug] <=> b[:slug] } }
it 'tests various stuff' do
- expect(subject.size).to eq 5
- expect(subject.first[:slug]).to eq('bands')
- expect(subject.first[:entries_custom_fields].size).to eq 5
- expect(subject.first[:entries_custom_fields].first[:position]).to eq 0
+ expect(subject.size).to eq 6
+ expect(subject[1][:slug]).to eq('bands')
+ expect(subject[1][:entries_custom_fields].size).to eq 5
+ expect(subject[1][:entries_custom_fields].first[:position]).to eq 0
end
end
spec/unit/adapters/filesystem/yaml_loaders/page_spec.rb +7 -7
@@ @@ -15,14 +15,14 @@ describe Locomotive::Steam::Adapters::Filesystem::YAMLLoaders::Page do
subject { loader.load(scope).sort { |a, b| a[:_fullpath] <=> b[:_fullpath] } }
it 'tests various stuff' do
- expect(subject.size).to eq 26
+ expect(subject.size).to eq 33
expect(subject.first[:title]).to eq(en: 'Page not found', fr: 'Page non trouvée')
- expect(subject[15][:is_layout]).to eq true
- expect(subject[15][:listed]).to eq false
- expect(subject[15][:published]).to eq false
- expect(subject[16][:slug]).to eq(en: 'music', fr: 'notre-musique')
- expect(subject[17][:_fullpath]).to eq 'songs'
- expect(subject[17][:template_path]).to eq(en: false)
+ expect(subject[22][:is_layout]).to eq true
+ expect(subject[22][:listed]).to eq false
+ expect(subject[22][:published]).to eq false
+ expect(subject[23][:slug]).to eq(en: 'music', fr: 'notre-musique')
+ expect(subject[24][:_fullpath]).to eq 'songs'
+ expect(subject[24][:template_path]).to eq(en: false)
end
end
spec/unit/adapters/filesystem/yaml_loaders/translation_spec.rb +1 -1
@@ @@ -15,7 +15,7 @@ describe Locomotive::Steam::Adapters::Filesystem::YAMLLoaders::Translation do
subject { loader.load(scope) }
it 'tests various stuff' do
- expect(subject.size).to eq 1
+ expect(subject.size).to eq 7
expect(subject.first[:key]).to eq('powered_by')
expect(subject.first[:values]).to eq({ 'en' => 'Powered by', 'fr' => 'Propulsé par' })
end
spec/unit/services/auth_service_spec.rb +129 -0
@@ @@ -0,0 +1,129 @@
+ require 'spec_helper'
+
+ describe Locomotive::Steam::AuthService do
+
+ let(:entries) { instance_double('ContentService') }
+ let(:emails) { instance_double('EmailService') }
+ let(:service) { described_class.new(entries, emails) }
+
+ let(:default_auth_options) { {
+ type: 'accounts',
+ id_field: 'email',
+ id: 'john@doe.net',
+ password_field: 'password',
+ password: 'easyone',
+ reset_password_url: '/reset-password',
+ reset_token: '42',
+ from: 'contact@acme.org',
+ subject: 'Instructions for changing your password',
+ email_handle: 'reset-password-email',
+ smtp: {}
+ } }
+
+ let(:auth_options) { instance_double('AuthOptions', default_auth_options) }
+
+ describe '#sign_in' do
+
+ subject { service.sign_in(auth_options) }
+
+ it 'returns :wrong_credentials if no entry matches the email' do
+ expect(entries).to receive(:all).with('accounts', { 'email' => 'john@doe.net' }).and_return([])
+ is_expected.to eq :wrong_credentials
+ end
+
+ it "returns :wrong_credentials if the password doesn't the entry's password" do
+ entry = build_account('fakeone')
+ expect(entries).to receive(:all).with('accounts', { 'email' => 'john@doe.net' }).and_return([entry])
+ is_expected.to eq :wrong_credentials
+ end
+
+ it "returns both :signed_in and the entry if the password matches the entry's password" do
+ entry = build_account('easyone')
+ expect(entries).to receive(:all).with('accounts', { 'email' => 'john@doe.net' }).and_return([entry])
+ is_expected.to eq [:signed_in, entry]
+ end
+
+ end
+
+ describe '#forgot_password' do
+
+ let(:liquid_context) { {} }
+
+ subject { service.forgot_password(auth_options, liquid_context) }
+
+ it 'returns :wrong_email if no entry matches the email' do
+ expect(entries).to receive(:all).with('accounts', { 'email' => 'john@doe.net' }).and_return([])
+ is_expected.to eq :wrong_email
+ end
+
+ it 'sends the instructions by email if an entry matches the email' do
+ allow(SecureRandom).to receive(:hex).and_return('42a')
+ entry = build_account('easyone', '42a')
+ expect(entries).to receive(:all).with('accounts', { 'email' => 'john@doe.net' }).and_return([entry])
+ expect(entries).to receive(:update_decorated_entry)
+ expect(emails).to receive(:send_email).with({
+ from: 'contact@acme.org',
+ to: 'john@doe.net',
+ subject: 'Instructions for changing your password',
+ page_handle: 'reset-password-email',
+ smtp: {} }, liquid_context)
+ is_expected.to eq :reset_password_instructions_sent
+ expect(liquid_context['reset_password_url']).to eq '/reset-password?auth_reset_token=42a'
+ end
+
+ end
+
+ describe '#reset_password' do
+
+ let(:_auth_options) { default_auth_options }
+ let(:auth_options) { instance_double('AuthOptions', _auth_options) }
+
+ subject { service.reset_password(auth_options) }
+
+ context 'no auth token' do
+
+ let(:_auth_options) { default_auth_options.merge({ reset_token: '' }) }
+ it { is_expected.to eq :invalid_token }
+
+ end
+
+ context 'password too short' do
+
+ let(:_auth_options) { default_auth_options.merge({ password: '' }) }
+ it { is_expected.to eq :password_too_short }
+
+ end
+
+ context 'expired auth token' do
+
+ it 'returns :invalid_token' do
+ entry = instance_double('Account', :[] => (Time.zone.now - 7.hours).iso8601)
+ expect(entries).to receive(:all).with('accounts', { '_auth_reset_token' => '42' }).and_return([entry])
+ is_expected.to eq :invalid_token
+ end
+
+ end
+
+ context 'valid auth token and password' do
+
+ it 'returns :password_reset and entry' do
+ entry = instance_double('Account', :[] => (Time.zone.now - 5.hours).iso8601)
+ expect(entries).to receive(:all).with('accounts', { '_auth_reset_token' => '42' }).and_return([entry])
+ expect(BCrypt::Password).to receive(:create).with('easyone').and_return('hashedeasyone')
+ expect(entries).to receive(:update_decorated_entry).with(entry, { 'password_hash' => 'hashedeasyone', '_auth_reset_token' => nil, '_auth_reset_sent_at' => nil })
+ is_expected.to eq [:password_reset, entry]
+ end
+
+ end
+
+ end
+
+ def build_account(password = 'easyone', reset_token = nil)
+ encrypted_password = BCrypt::Password.create(password)
+ entry = instance_double('Account', password: BCrypt::Password.new(encrypted_password))
+ allow(entry).to receive(:[]).with(:password_hash).and_return(encrypted_password)
+ allow(entry).to receive(:[]).with('_auth_reset_token').and_return(reset_token)
+ entry
+ end
+
+ end