diff --git a/app/controllers/alchemy/accounts_controller.rb b/app/controllers/alchemy/accounts_controller.rb new file mode 100644 index 00000000..ba8a843d --- /dev/null +++ b/app/controllers/alchemy/accounts_controller.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Alchemy + class AccountsController < ::Devise::RegistrationsController + helper "Alchemy::Pages" + + def show + authorize! :show, current_alchemy_user + @user = current_alchemy_user + end + + private + + def permission_denied(*) + store_location_for(:user, account_path) + flash[:warning] = t(:unauthenticated, scope: "devise.failure") + redirect_to alchemy.login_path + end + end +end diff --git a/app/controllers/alchemy/admin/passwords_controller.rb b/app/controllers/alchemy/admin/passwords_controller.rb index df569e81..77d351a1 100644 --- a/app/controllers/alchemy/admin/passwords_controller.rb +++ b/app/controllers/alchemy/admin/passwords_controller.rb @@ -22,16 +22,8 @@ def new_session_path(resource_name) alchemy.admin_login_path end - def admin_edit_password_url(_resource, options = {}) - alchemy.admin_edit_password_url(options) - end - def after_resetting_password_path_for(resource) - if can? :index, :alchemy_admin_dashboard - alchemy.admin_dashboard_path - else - alchemy.root_path - end + alchemy.admin_dashboard_path end end end diff --git a/app/controllers/alchemy/confirmations_controller.rb b/app/controllers/alchemy/confirmations_controller.rb new file mode 100644 index 00000000..354b1ed4 --- /dev/null +++ b/app/controllers/alchemy/confirmations_controller.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Alchemy + class ConfirmationsController < ::Devise::ConfirmationsController + helper "Alchemy::Pages" + + private + + def new_session_path(*) + alchemy.login_path + end + end +end diff --git a/app/controllers/alchemy/passwords_controller.rb b/app/controllers/alchemy/passwords_controller.rb new file mode 100644 index 00000000..c90176fc --- /dev/null +++ b/app/controllers/alchemy/passwords_controller.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Alchemy + class PasswordsController < ::Devise::PasswordsController + helper "Alchemy::Pages" + + private + + def new_session_path(*) + alchemy.login_path + end + end +end diff --git a/app/controllers/alchemy/sessions_controller.rb b/app/controllers/alchemy/sessions_controller.rb new file mode 100644 index 00000000..89954af0 --- /dev/null +++ b/app/controllers/alchemy/sessions_controller.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Alchemy + class SessionsController < ::Devise::SessionsController + helper "Alchemy::Pages" + + private + + def after_sign_in_path_for(user) + stored_location_for(user) || alchemy.account_path + end + end +end diff --git a/app/mailers/alchemy/notifications.rb b/app/mailers/alchemy/notifications.rb index 33327df3..d2fede33 100644 --- a/app/mailers/alchemy/notifications.rb +++ b/app/mailers/alchemy/notifications.rb @@ -1,14 +1,15 @@ -module Alchemy - class Notifications < ActionMailer::Base +# frozen_string_literal: true - default(from: Config.get(:mailer)['mail_from']) +module Alchemy + class Notifications < ::Devise::Mailer + default(from: Config.get(:mailer)["mail_from"]) def member_created(user) @user = user mail( to: user.email, - subject: Alchemy.t("Your user credentials") + subject: Alchemy.t("Your user credentials"), ) end @@ -17,16 +18,34 @@ def alchemy_user_created(user) @url = admin_url mail( to: user.email, - subject: Alchemy.t("Your Alchemy Login") + subject: Alchemy.t("Your Alchemy Login"), + ) + end + + def member_reset_password_instructions(user, token, _opts = {}) + @user = user + @token = token + mail( + to: user.email, + subject: Alchemy.t("Reset password instructions"), + ) + end + + def reset_password_instructions(user, token, _opts = {}) + @user = user + @token = token + mail( + to: user.email, + subject: Alchemy.t("Reset password instructions"), ) end - def reset_password_instructions(user, token, opts={}) + def confirmation_instructions(user, token, _opts = {}) @user = user @token = token mail( to: user.email, - subject: Alchemy.t("Reset password instructions") + subject: Alchemy.t("Account confirmation instructions"), ) end end diff --git a/app/models/alchemy/user.rb b/app/models/alchemy/user.rb index bd574c88..d423f5de 100644 --- a/app/models/alchemy/user.rb +++ b/app/models/alchemy/user.rb @@ -1,5 +1,7 @@ -require 'devise/orm/active_record' -require 'userstamp' +# frozen_string_literal: true + +require "devise/orm/active_record" +require "userstamp" module Alchemy class User < ActiveRecord::Base @@ -12,7 +14,7 @@ class User < ActiveRecord::Base :password, :password_confirmation, :send_credentials, - :tag_list + :tag_list, ] devise *Alchemy.devise_modules @@ -29,9 +31,9 @@ class User < ActiveRecord::Base # Unlock all locked pages before destroy. before_destroy :unlock_pages! - scope :admins, -> { where(arel_table[:alchemy_roles].matches('%admin%')) } - scope :logged_in, -> { where('last_request_at > ?', logged_in_timeout.seconds.ago) } - scope :logged_out, -> { where('last_request_at is NULL or last_request_at <= ?', logged_in_timeout.seconds.ago) } + scope :admins, -> { where(arel_table[:alchemy_roles].matches("%admin%")) } + scope :logged_in, -> { where("last_request_at > ?", logged_in_timeout.seconds.ago) } + scope :logged_out, -> { where("last_request_at is NULL or last_request_at <= ?", logged_in_timeout.seconds.ago) } ROLES = Config.get(:user_roles) @@ -51,10 +53,7 @@ def logged_in_timeout def search(query) query = "%#{query.downcase}%" - where arel_table[:login].lower.matches(query) - .or arel_table[:email].lower.matches(query) - .or arel_table[:firstname].lower.matches(query) - .or arel_table[:lastname].lower.matches(query) + where arel_table[:login].lower.matches(query).or arel_table[:email].lower.matches(query).or arel_table[:firstname].lower.matches(query).or arel_table[:lastname].lower.matches(query) end end @@ -67,12 +66,12 @@ def role end def alchemy_roles - read_attribute(:alchemy_roles).split(' ') + read_attribute(:alchemy_roles).split(" ") end def alchemy_roles=(roles_string) if roles_string.is_a? Array - write_attribute(:alchemy_roles, roles_string.join(' ')) + write_attribute(:alchemy_roles, roles_string.join(" ")) elsif roles_string.is_a? String write_attribute(:alchemy_roles, roles_string) end @@ -84,8 +83,9 @@ def add_role(role) # Returns true if the user ahs admin role def is_admin? - has_role? 'admin' + has_role? "admin" end + alias_method :admin?, :is_admin? # Returns true if the user has the given role. @@ -105,6 +105,7 @@ def unlock_pages! def pages_locked_by_me Page.locked_by(self).order(:updated_at) end + alias_method :locked_pages, :pages_locked_by_me # Returns the firstname and lastname as a string @@ -118,11 +119,12 @@ def fullname(options = {}) if lastname.blank? && firstname.blank? login else - options = {:flipped => false}.merge(options) + options = { flipped: false }.merge(options) fullname = options[:flipped] ? "#{lastname}, #{firstname}" : "#{firstname} #{lastname}" fullname.squeeze(" ").strip end end + alias_method :name, :fullname alias_method :alchemy_display_name, :fullname @@ -150,13 +152,22 @@ def store_request_time! # Delivers a welcome mail depending from user's role. # def deliver_welcome_mail - if has_role?('author') || has_role?('editor') || has_role?('admin') + if has_role?("author") || has_role?("editor") || has_role?("admin") Notifications.alchemy_user_created(self).deliver_later else Notifications.member_created(self).deliver_later end end + # Overwritten to send a different email to members + def send_reset_password_instructions_notification(token) + if has_role?("member") + send_devise_notification(:member_reset_password_instructions, token, {}) + else + send_devise_notification(:reset_password_instructions, token, {}) + end + end + private def logged_in_timeout diff --git a/app/views/alchemy/accounts/show.html.erb b/app/views/alchemy/accounts/show.html.erb new file mode 100644 index 00000000..a5c0a172 --- /dev/null +++ b/app/views/alchemy/accounts/show.html.erb @@ -0,0 +1,3 @@ +

Hallo <%= @user.fullname %>

+ +<%= link_to 'Edit account', alchemy.edit_account_path %> diff --git a/app/views/alchemy/devise/shared/_links.html.erb b/app/views/alchemy/devise/shared/_links.html.erb new file mode 100644 index 00000000..cd17113f --- /dev/null +++ b/app/views/alchemy/devise/shared/_links.html.erb @@ -0,0 +1,15 @@ +<%- if controller_name != 'sessions' %> + <%= link_to t('.login', default: 'Log in'), alchemy.login_path %>
+<% end %> + +<%- if devise_mapping.registerable? && controller_name != 'registrations' %> + <%= link_to t('.sign_up', default: 'Sign up'), alchemy.new_account_path %>
+<% end %> + +<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %> + <%= link_to t('.forgot_password', default: 'Forgot your password?'), alchemy.new_password_path %>
+<% end %> + +<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %> + <%= link_to t('.confirmation_instructions', default: "Didn't receive confirmation instructions?"), alchemy.new_confirmation_path %>
+<% end %> diff --git a/app/views/alchemy/notifications/confirmation_instructions.de.text.erb b/app/views/alchemy/notifications/confirmation_instructions.de.text.erb new file mode 100644 index 00000000..d96c9e19 --- /dev/null +++ b/app/views/alchemy/notifications/confirmation_instructions.de.text.erb @@ -0,0 +1,5 @@ +Willkommen <%= @user.fullname %>! + +Bitte bestätigen Sie Ihre E-Mail-Adresse durch klicken auf folgenden Link: + +<%= alchemy.confirmation_url(@user, confirmation_token: @token) %> diff --git a/app/views/alchemy/notifications/confirmation_instructions.en.text.erb b/app/views/alchemy/notifications/confirmation_instructions.en.text.erb new file mode 100644 index 00000000..1aacf311 --- /dev/null +++ b/app/views/alchemy/notifications/confirmation_instructions.en.text.erb @@ -0,0 +1,5 @@ +Welcome <%= @user.fullname %>! + +You can confirm your account email through the link below: + +<%= alchemy.confirmation_url(@user, confirmation_token: @token) %> diff --git a/app/views/alchemy/notifications/member_reset_password_instructions.de.text.erb b/app/views/alchemy/notifications/member_reset_password_instructions.de.text.erb new file mode 100644 index 00000000..f306fa0b --- /dev/null +++ b/app/views/alchemy/notifications/member_reset_password_instructions.de.text.erb @@ -0,0 +1,8 @@ +Hallo <%= @user.fullname %>. + +Sie haben angefordert Ihr Passwort zurückzusetzen. Dies kann durch anklicken des nachfolgenden Links bestätigt werden. + +<%= alchemy.edit_password_url(@user, reset_password_token: @token) %> + +Wenn Sie diese Zurücksetzung nicht angefragt haben, dann können Sie diese E-Mail einfach ignorieren. +Ihr Passwort wird erst dann zurückgesetzt, wenn Sie den Link anklicken. diff --git a/app/views/alchemy/notifications/member_reset_password_instructions.en.text.erb b/app/views/alchemy/notifications/member_reset_password_instructions.en.text.erb new file mode 100644 index 00000000..e1150cbf --- /dev/null +++ b/app/views/alchemy/notifications/member_reset_password_instructions.en.text.erb @@ -0,0 +1,8 @@ +Hello <%= @user.name %>. + +You have requested to change your password. Please confirm this by clicking the link below. + +<%= alchemy.edit_password_url(@user, reset_password_token: @token) %> + +If you didn't request this, please ignore this email. +Your password won't change until you access the link above and create a new one. diff --git a/app/views/alchemy/notifications/member_reset_password_instructions.es.text.erb b/app/views/alchemy/notifications/member_reset_password_instructions.es.text.erb new file mode 100644 index 00000000..55d62afc --- /dev/null +++ b/app/views/alchemy/notifications/member_reset_password_instructions.es.text.erb @@ -0,0 +1,8 @@ +Hola <%= @user.name %>. + +Has solicitado modificar tu contraseña. Por favor, confírmalo pulsando en el siguiente enlace. + +<%= alchemy.edit_password_url(@user, reset_password_token: @token) %> + +Si no has sido tu el que ha hecho la solicitud, ignora este correo. +Tu contraseña no cambiará hasta que no accedas al enlace de arriba y generes una nueva. diff --git a/app/views/alchemy/notifications/member_reset_password_instructions.ru.text.erb b/app/views/alchemy/notifications/member_reset_password_instructions.ru.text.erb new file mode 100644 index 00000000..c4ecfdca --- /dev/null +++ b/app/views/alchemy/notifications/member_reset_password_instructions.ru.text.erb @@ -0,0 +1,8 @@ +Здравствуйте, <%= @user.name %>. + +Вы сделали запрос на смену пароля. Пожалуйста подтвердите это, нажав на ссылку ниже. + +<%= alchemy.edit_password_url(@user, reset_password_token: @token) %> + +Если вы не делали запрос, просто проигнорируйте это письмо. +Ваш пароль не изменится до тех пор, пока вы не перейдете по ссылке и сами не измените его. diff --git a/app/views/alchemy/passwords/edit.html.erb b/app/views/alchemy/passwords/edit.html.erb new file mode 100644 index 00000000..786a7a88 --- /dev/null +++ b/app/views/alchemy/passwords/edit.html.erb @@ -0,0 +1,27 @@ +

<%= t('.title', default: 'Change your password') %>

+ +<%= simple_form_for(@user, as: :user, url: alchemy.password_path, html: { method: :put }) do |f| %> + <%= f.error_notification %> + + <%= f.input :reset_password_token, as: :hidden %> + <%= f.full_error :reset_password_token %> + +
+ <%= f.input :password, + label: t('.password.label', default: 'New password'), + required: true, + autofocus: true, + hint: t('.hint', default: '%{minimum} characters minimum', minimum: @minimum_password_length) if @minimum_password_length, + input_html: { autocomplete: "new-password" } %> + <%= f.input :password_confirmation, + label: t('.password_confirmation.label', default: 'Confirm your new password'), + required: true, + input_html: { autocomplete: "new-password" } %> +
+ +
+ <%= f.button :submit, t('.button.label', default: 'Change my password') %> +
+<% end %> + +<%= render "alchemy/devise/shared/links" %> diff --git a/app/views/alchemy/passwords/new.html.erb b/app/views/alchemy/passwords/new.html.erb new file mode 100644 index 00000000..30090daa --- /dev/null +++ b/app/views/alchemy/passwords/new.html.erb @@ -0,0 +1,18 @@ +

<%= t('.title', default: 'Forgot your password?') %>

+ +<%= simple_form_for(@user, as: :user, url: alchemy.password_path, html: { method: :post }) do |f| %> + <%= f.error_notification %> + +
+ <%= f.input :email, + required: true, + autofocus: true, + input_html: { autocomplete: "email" } %> +
+ +
+ <%= f.button :submit, t('.button.label', default: 'Send me reset password instructions') %> +
+<% end %> + +<%= render "alchemy/devise/shared/links" %> diff --git a/app/views/alchemy/sessions/new.html.erb b/app/views/alchemy/sessions/new.html.erb new file mode 100644 index 00000000..5b5954b6 --- /dev/null +++ b/app/views/alchemy/sessions/new.html.erb @@ -0,0 +1,32 @@ +

<%= t('.title', default: 'Log in') %>

+ +<% if flash[:alert] %> +
+ <%= flash[:alert] %> +
+<% end %> + +<% if flash[:notice] %> +
+ <%= flash[:notice] %> +
+<% end %> + +<%= simple_form_for(@user, as: :user, url: alchemy.login_path) do |f| %> +
+ <%= f.input :email, + required: false, + autofocus: true, + input_html: { autocomplete: "email" } %> + <%= f.input :password, + required: false, + input_html: { autocomplete: "current-password" } %> + <%= f.input :remember_me, as: :boolean if devise_mapping.rememberable? %> +
+ +
+ <%= f.button :submit, t('.button.label', default: 'Log in') %> +
+<% end %> + +<%= render "alchemy/devise/shared/links" %> diff --git a/config/routes.rb b/config/routes.rb index 055cce5c..c3cfbe78 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,37 +1,70 @@ +# frozen_string_literal: true + Alchemy::Engine.routes.draw do + if Alchemy::Devise.enable_user_accounts? + devise_for :user, + class_name: "Alchemy::User", + singular: :user, + skip: :all, + controllers: { + registrations: Alchemy::Devise.registrations_enabled? ? "alchemy/accounts" : nil, + confirmations: Alchemy::Devise.confirmations_enabled? ? "alchemy/confirmations" : nil, + sessions: "alchemy/sessions", + passwords: "alchemy/passwords", + }, + path: :account, + router_name: :alchemy + + scope :account do + devise_scope :user do + get "/login" => "sessions#new" + post "/login" => "sessions#create" + match "/logout" => "sessions#destroy", via: Devise.sign_out_via + + if Alchemy::Devise.confirmations_enabled? + resource :confirmation, only: %i[new create show] + end + resource :password, only: %i[new create edit update] + end + end + + devise_scope :user do + resource :account, except: Alchemy::Devise.registrations_enabled? ? [] : %i[new create] + end + end + namespace :admin, { path: Alchemy.admin_path, - constraints: Alchemy.admin_constraints + constraints: Alchemy.admin_constraints, } do - devise_for :user, - class_name: 'Alchemy::User', + class_name: "Alchemy::User", singular: :user, skip: :all, controllers: { - sessions: 'alchemy/admin/user_sessions', - passwords: 'alchemy/admin/passwords' + sessions: "alchemy/admin/user_sessions", + passwords: "alchemy/admin/passwords", }, router_name: :alchemy devise_scope :user do - get '/dashboard' => 'dashboard#index', + get "/dashboard" => "dashboard#index", :as => :user_root - get '/signup' => 'users#signup', + get "/signup" => "users#signup", :as => :signup - get '/login' => 'user_sessions#new', + get "/login" => "user_sessions#new", :as => :login - post '/login' => 'user_sessions#create' - match '/logout' => 'user_sessions#destroy', + post "/login" => "user_sessions#create" + match "/logout" => "user_sessions#destroy", :as => :logout, via: Devise.sign_out_via - get '/passwords' => 'passwords#new', + get "/passwords" => "passwords#new", :as => :new_password - get '/passwords/:id/edit/:reset_password_token' => 'passwords#edit', + get "/passwords/:id/edit/:reset_password_token" => "passwords#edit", :as => :edit_password - post '/passwords' => 'passwords#create', + post "/passwords" => "passwords#create", :as => :reset_password - patch '/passwords' => 'passwords#update', + patch "/passwords" => "passwords#update", :as => :update_password end diff --git a/lib/alchemy/devise.rb b/lib/alchemy/devise.rb index 65eb8a28..658e1ddb 100644 --- a/lib/alchemy/devise.rb +++ b/lib/alchemy/devise.rb @@ -6,12 +6,12 @@ module Alchemy # === Default modules # # [ - #. :database_authenticatable, + # :database_authenticatable, # :trackable, # :validatable, # :timeoutable, # :recoverable - #. ] + # ] # # If you want to add additional modules into the Alchemy user class append # them to this collection in an initializer in your app. @@ -36,6 +36,25 @@ def self.devise_modules ] end + def self.devise_modules=(modules) + @devise_modules = modules + end + module Devise + def self.enable_user_accounts? + @enable_user_accounts ||= false + end + + def self.enable_user_accounts=(val) + @enable_user_accounts = val + end + + def self.registrations_enabled? + Alchemy.devise_modules.include?(:registerable) + end + + def self.confirmations_enabled? + Alchemy.devise_modules.include?(:confirmable) + end end end diff --git a/spec/controllers/accounts_controller_spec.rb b/spec/controllers/accounts_controller_spec.rb new file mode 100644 index 00000000..0122c45f --- /dev/null +++ b/spec/controllers/accounts_controller_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require "rails_helper" + +describe Alchemy::AccountsController do + routes { Alchemy::Engine.routes } + + context "with user accounts enabled" do + before do + allow(Alchemy::Devise).to receive(:enable_user_accounts?) { true } + Rails.application.reload_routes! + @request.env["devise.mapping"] = Devise.mappings[:user] + end + + describe "#show" do + let(:user) { create(:alchemy_member_user) } + + context "with authorized user" do + before { authorize_user(user) } + + render_views + + it "shows account" do + get :show + is_expected.to render_template(:show) + end + end + + context "with unauthorized user" do + it "redirects to login" do + get :show + is_expected.to redirect_to(login_path) + end + + it "stores current location" do + get :show + expect(session[:user_return_to]).to eq(account_path) + end + + it "shows warning message" do + get :show + expect(flash[:warning]).to eq I18n.t(:unauthenticated, scope: "devise.failure") + end + end + end + end +end diff --git a/spec/controllers/confirmations_controller_spec.rb b/spec/controllers/confirmations_controller_spec.rb new file mode 100644 index 00000000..21f9879a --- /dev/null +++ b/spec/controllers/confirmations_controller_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require "rails_helper" + +describe Alchemy::ConfirmationsController do + routes { Alchemy::Engine.routes } + + context "with user accounts enabled" do + before do + allow(Alchemy::Devise).to receive(:enable_user_accounts?) { true } + end + + context "with confirmations enabled" do + let(:user) { double(email: "mail@example.com") } + + before do + allow(Alchemy::Devise).to receive(:confirmations_enabled?) { true } + Rails.application.reload_routes! + @request.env["devise.mapping"] = Devise.mappings[:user] + expect(Alchemy::User).to receive(:send_confirmation_instructions) { user } + end + + describe "#create" do + context "with valid params" do + before do + expect(user).to receive(:errors) { [] } + end + + it "redirects to account" do + post :create, params: { user: { email: user.email } } + expect(response).to redirect_to(login_path) + end + end + + context "without valid params" do + before do + expect(user).to receive(:errors).twice { ["Email not found"] } + end + + it "renders form" do + post :create, params: { user: { email: "not@found" } } + is_expected.to render_template(:new) + end + end + end + end + end +end diff --git a/spec/controllers/passwords_controller_spec.rb b/spec/controllers/passwords_controller_spec.rb new file mode 100644 index 00000000..79d3a223 --- /dev/null +++ b/spec/controllers/passwords_controller_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require "rails_helper" + +describe Alchemy::PasswordsController do + routes { Alchemy::Engine.routes } + + context "with user accounts enabled" do + before(:all) do + Alchemy::Devise.enable_user_accounts = true + Rails.application.reload_routes! + end + + before do + @request.env["devise.mapping"] = Devise.mappings[:user] + end + + let!(:user) { create(:alchemy_user) } + + describe "#create" do + context "with valid params" do + it "redirects to login" do + post :create, params: { user: { email: user.email } } + expect(response).to redirect_to(login_path) + end + end + + context "without valid params" do + it "renders form" do + post :create, params: { user: { email: "not@found" } } + is_expected.to render_template(:new) + end + end + end + end +end diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb new file mode 100644 index 00000000..497da995 --- /dev/null +++ b/spec/controllers/sessions_controller_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require "rails_helper" + +describe Alchemy::SessionsController do + routes { Alchemy::Engine.routes } + + context "with user accounts enabled" do + before(:all) do + Alchemy::Devise.enable_user_accounts = true + Rails.application.reload_routes! + end + + before do + @request.env["devise.mapping"] = Devise.mappings[:user] + end + + let(:user) { create(:alchemy_user) } + + describe "#create" do + context "with valid user" do + let(:user_params) do + { + login: user.login, + password: "s3cr3t", + } + end + + before { user } + + context "without redirect path in session" do + it "redirects to account" do + post :create, params: { user: user_params } + expect(response).to redirect_to(account_path) + end + end + + context "with redirect path in session" do + it "redirects to these params" do + session[:user_return_to] = "/secret_page" + post :create, params: { user: user_params } + expect(response).to redirect_to("/secret_page") + end + end + + context "without valid params" do + it "renders login form" do + post :create, params: { user: { login: "" } } + is_expected.to render_template(:new) + end + end + end + end + + describe "#destroy" do + before do + authorize_user(user) + end + + it "redirects to root" do + delete :destroy + is_expected.to redirect_to(root_path) + end + end + end +end diff --git a/spec/features/login_feature_spec.rb b/spec/features/admin/login_feature_spec.rb similarity index 98% rename from spec/features/login_feature_spec.rb rename to spec/features/admin/login_feature_spec.rb index da7a6fe8..1742e56c 100644 --- a/spec/features/login_feature_spec.rb +++ b/spec/features/admin/login_feature_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -describe "Login: " do +describe "Admin Login: " do context "If user is present" do let!(:user) do Alchemy::User.create!( diff --git a/spec/features/password_reset_feature_spec.rb b/spec/features/admin/password_reset_feature_spec.rb similarity index 87% rename from spec/features/password_reset_feature_spec.rb rename to spec/features/admin/password_reset_feature_spec.rb index 30d1d3d4..a537fcc7 100644 --- a/spec/features/password_reset_feature_spec.rb +++ b/spec/features/admin/password_reset_feature_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -describe "Password reset feature." do +describe "Admin password reset feature." do let(:user) { create(:alchemy_admin_user) } it "User can visit password reset form." do @@ -20,7 +20,7 @@ end it "User can change password." do - allow(Alchemy::User) + expect(Alchemy::User) .to receive(:reset_password_by_token) .and_return(user) @@ -30,6 +30,8 @@ fill_in :user_password_confirmation, with: 'secret123' click_button 'Change password' + expect(page.current_path).to eq(alchemy.admin_dashboard_path) + expect(page) .to have_content('Your password has been changed successfully.') end diff --git a/spec/mailers/notifications_spec.rb b/spec/mailers/notifications_spec.rb index 377735b9..705467b6 100644 --- a/spec/mailers/notifications_spec.rb +++ b/spec/mailers/notifications_spec.rb @@ -1,21 +1,22 @@ -require 'rails_helper' +# frozen_string_literal: true + +require "rails_helper" module Alchemy describe Notifications do - context "when a member user was created" do let(:user) do - mock_model 'User', + mock_model "User", alchemy_roles: %w(member), - email: 'jon@doe.com', - name: 'John Doe', - login: 'jon.doe' + email: "jon@doe.com", + name: "John Doe", + login: "jon.doe" end let(:mail) { Notifications.member_created(user) } it "delivers a mail to user" do expect(mail.to).to eq([user.email]) - expect(mail.subject).to eq('Your user credentials') + expect(mail.subject).to eq("Your user credentials") end it "mail body includes users name" do @@ -32,12 +33,12 @@ module Alchemy end context "when an admin user was created" do - let(:user) { mock_model('User', alchemy_roles: %w(admin), email: 'jon@doe.com', name: 'John Doe', login: 'jon.doe') } + let(:user) { mock_model("User", alchemy_roles: %w(admin), email: "jon@doe.com", name: "John Doe", login: "jon.doe") } let(:mail) { Notifications.alchemy_user_created(user) } it "delivers a mail to user" do expect(mail.to).to eq([user.email]) - expect(mail.subject).to eq('Your Alchemy Login') + expect(mail.subject).to eq("Your Alchemy Login") end it "mail body includes users login" do @@ -49,17 +50,17 @@ module Alchemy end end - describe '#reset_password_instructions' do + describe "#reset_password_instructions" do let(:user) do - mock_model 'User', + mock_model "User", alchemy_roles: %w(member), - email: 'jon@doe.com', - name: 'John Doe', - login: 'jon.doe', - fullname: 'John Doe' + email: "jon@doe.com", + name: "John Doe", + login: "jon.doe", + fullname: "John Doe" end - let(:token) { '123456789' } + let(:token) { "123456789" } let(:mail) do Notifications.reset_password_instructions(user, token) @@ -67,7 +68,7 @@ module Alchemy it "delivers a mail to user" do expect(mail.to).to eq([user.email]) - expect(mail.subject).to eq('Reset password instructions') + expect(mail.subject).to eq("Reset password instructions") end it "mail body includes users name" do @@ -78,5 +79,43 @@ module Alchemy expect(mail.body.raw_source).to match /#{Regexp.escape(admin_edit_password_url(user, reset_password_token: token, only_path: true))}/ end end + + context "with user accounts and confirmations enabled" do + before do + allow(Alchemy::Devise).to receive(:enable_user_accounts?) { true } + allow(Alchemy::Devise).to receive(:confirmations_enabled?) { true } + Rails.application.reload_routes! + end + + describe "#confirmation_instructions" do + let(:user) do + mock_model "User", + alchemy_roles: %w(member), + email: "jon@doe.com", + name: "John Doe", + login: "jon.doe", + fullname: "John Doe" + end + + let(:token) { "123456789" } + + let(:mail) do + Notifications.confirmation_instructions(user, token) + end + + it "delivers a mail to user" do + expect(mail.to).to eq([user.email]) + expect(mail.subject).to eq("Account confirmation instructions") + end + + it "mail body includes users name" do + expect(mail.body.raw_source).to match /#{user.fullname}/ + end + + it "mail body includes reset instructions" do + expect(mail.body.raw_source).to match /#{Regexp.escape(confirmation_url(user, confirmation_token: token, only_path: true))}/ + end + end + end end end diff --git a/spec/routing/account_routing_spec.rb b/spec/routing/account_routing_spec.rb new file mode 100644 index 00000000..a2212986 --- /dev/null +++ b/spec/routing/account_routing_spec.rb @@ -0,0 +1,134 @@ +# frozen_string_literal: true + +require "rails_helper" + +describe "Account Routing" do + context "if user accounts are enabled" do + before(:all) do + Alchemy::Devise.enable_user_accounts = true + Rails.application.reload_routes! + end + + it "routes to show account" do + expect({ + get: "/account", + }).to route_to( + controller: "alchemy/accounts", + action: "show", + ) + end + + it "routes to edit account" do + expect({ + get: "/account/edit", + }).to route_to( + controller: "alchemy/accounts", + action: "edit", + ) + end + + it "routes to update account" do + expect({ + patch: "/account", + }).to route_to( + controller: "alchemy/accounts", + action: "update", + ) + end + + it "routes to destroy account" do + expect({ + delete: "/account", + }).to route_to( + controller: "alchemy/accounts", + action: "destroy", + ) + end + + context "when registrations are enabled" do + before do + allow(Alchemy::Devise).to receive(:registrations_enabled?) { true } + Rails.application.reload_routes! + end + + it "routes to new account" do + expect({ + get: "/account/new", + }).to route_to( + controller: "alchemy/accounts", + action: "new", + ) + end + + it "routes to create account" do + expect({ + post: "/account", + }).to route_to( + controller: "alchemy/accounts", + action: "create", + ) + end + end + end + + context "if user accounts are disabled" do + before(:all) do + Alchemy::Devise.enable_user_accounts = false + Rails.application.reload_routes! + end + + it "does not route to show account" do + expect({ + get: "/account", + }).to_not route_to( + controller: "alchemy/accounts", + action: "show", + ) + end + + it "does not route to edit account" do + expect({ + get: "/account/edit", + }).to_not route_to( + controller: "alchemy/accounts", + action: "edit", + ) + end + + it "does not route to update account" do + expect({ + patch: "/account", + }).to_not route_to( + controller: "alchemy/accounts", + action: "update", + ) + end + + it "does not route to destroy account" do + expect({ + delete: "/account", + }).to_not route_to( + controller: "alchemy/accounts", + action: "destroy", + ) + end + + it "does not route to new account" do + expect({ + get: "/account/new", + }).to_not route_to( + controller: "alchemy/accounts", + action: "new", + ) + end + + it "does not route to create account" do + expect({ + post: "/account", + }).to_not route_to( + controller: "alchemy/accounts", + action: "create", + ) + end + end +end diff --git a/spec/routing/admin_password_routing_spec.rb b/spec/routing/admin_password_routing_spec.rb new file mode 100644 index 00000000..7d0f372e --- /dev/null +++ b/spec/routing/admin_password_routing_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require "rails_helper" + +describe "Admin Password Routing" do + routes { Alchemy::Engine.routes } + + it "routes to new password" do + expect({ + get: "/admin/passwords", + }).to route_to( + controller: "alchemy/admin/passwords", + action: "new", + ) + end + + it "routes to reset password" do + expect({ + post: "/admin/passwords", + }).to route_to( + controller: "alchemy/admin/passwords", + action: "create", + ) + end + + it "routes to edit password" do + expect({ + get: "/admin/passwords/123/edit/12345", + }).to route_to( + controller: "alchemy/admin/passwords", + action: "edit", + id: "123", + reset_password_token: "12345", + ) + end + + it "routes to update password" do + expect({ + patch: "/admin/passwords", + }).to route_to( + controller: "alchemy/admin/passwords", + action: "update", + ) + end +end diff --git a/spec/routing/admin_user_session_routing_spec.rb b/spec/routing/admin_user_session_routing_spec.rb new file mode 100644 index 00000000..49d267d7 --- /dev/null +++ b/spec/routing/admin_user_session_routing_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require "rails_helper" + +describe "Admin User Session Routing" do + routes { Alchemy::Engine.routes } + + it "routes to login" do + expect({ + get: "/admin/login", + }).to route_to( + controller: "alchemy/admin/user_sessions", + action: "new", + ) + end + + it "routes to create session" do + expect({ + post: "/admin/login", + }).to route_to( + controller: "alchemy/admin/user_sessions", + action: "create", + ) + end + + it "routes to logout" do + expect({ + delete: "/admin/logout", + }).to route_to( + controller: "alchemy/admin/user_sessions", + action: "destroy", + ) + end +end diff --git a/spec/routing/password_routing_spec.rb b/spec/routing/password_routing_spec.rb index 008e577e..d5e3839b 100644 --- a/spec/routing/password_routing_spec.rb +++ b/spec/routing/password_routing_spec.rb @@ -1,43 +1,91 @@ -require 'rails_helper' +# frozen_string_literal: true + +require "rails_helper" describe "Password Routing" do - routes { Alchemy::Engine.routes } - - it "routes to new password" do - expect({ - get: "/admin/passwords" - }).to route_to( - controller: "alchemy/admin/passwords", - action: "new" - ) - end + context "if user accounts are enabled" do + before(:all) do + Alchemy::Devise.enable_user_accounts = true + Rails.application.reload_routes! + end - it "routes to reset password" do - expect({ - post: "/admin/passwords" - }).to route_to( - controller: "alchemy/admin/passwords", - action: "create" - ) - end + it "routes to new password" do + expect({ + get: "/account/password/new", + }).to route_to( + controller: "alchemy/passwords", + action: "new", + ) + end + + it "routes to reset password" do + expect({ + post: "/account/password", + }).to route_to( + controller: "alchemy/passwords", + action: "create", + ) + end + + it "routes to edit password" do + expect({ + get: "/account/password/edit", + }).to route_to( + controller: "alchemy/passwords", + action: "edit", + ) + end - it "routes to edit password" do - expect({ - get: "/admin/passwords/123/edit/12345" - }).to route_to( - controller: "alchemy/admin/passwords", - action: "edit", - id: "123", - reset_password_token: "12345" - ) + it "routes to update password" do + expect({ + patch: "/account/password", + }).to route_to( + controller: "alchemy/passwords", + action: "update", + ) + end end - it "routes to update password" do - expect({ - patch: "/admin/passwords" - }).to route_to( - controller: "alchemy/admin/passwords", - action: "update" - ) + context "if user accounts are disabled" do + before(:all) do + Alchemy::Devise.enable_user_accounts = false + Rails.application.reload_routes! + end + + it "does not route to new password" do + expect({ + get: "/account/password/new", + }).to_not route_to( + controller: "alchemy/passwords", + action: "new", + ) + end + + it "does not route to reset password" do + expect({ + post: "/account/password", + }).to_not route_to( + controller: "alchemy/passwords", + action: "create", + ) + end + + it "does not route to edit password" do + expect({ + get: "/account/password/edit", + }).to_not route_to( + controller: "alchemy/passwords", + action: "edit", + ) + end + + it "does not route to update password" do + expect({ + patch: "/account/password", + }).to_not route_to( + controller: "alchemy/passwords", + action: "update", + ) + end end end diff --git a/spec/routing/session_routing_spec.rb b/spec/routing/session_routing_spec.rb index 0d6b9a9e..304a8da5 100644 --- a/spec/routing/session_routing_spec.rb +++ b/spec/routing/session_routing_spec.rb @@ -1,32 +1,73 @@ -require 'rails_helper' +# frozen_string_literal: true + +require "rails_helper" describe "Session Routing" do - routes { Alchemy::Engine.routes } - - it "routes to login" do - expect({ - get: "/admin/login" - }).to route_to( - controller: "alchemy/admin/user_sessions", - action: "new" - ) - end + context "if user accounts are enabled" do + before(:all) do + Alchemy::Devise.enable_user_accounts = true + Rails.application.reload_routes! + end + + it "routes to login" do + expect({ + get: "/account/login", + }).to route_to( + controller: "alchemy/sessions", + action: "new", + ) + end + + it "routes to create session" do + expect({ + post: "/account/login", + }).to route_to( + controller: "alchemy/sessions", + action: "create", + ) + end - it "routes to create session" do - expect({ - post: "/admin/login" - }).to route_to( - controller: "alchemy/admin/user_sessions", - action: "create" - ) + it "routes to logout" do + expect({ + delete: "/account/logout", + }).to route_to( + controller: "alchemy/sessions", + action: "destroy", + ) + end end - it "routes to logout" do - expect({ - delete: "/admin/logout" - }).to route_to( - controller: "alchemy/admin/user_sessions", - action: "destroy" - ) + context "if user accounts are disabled" do + before(:all) do + Alchemy::Devise.enable_user_accounts = false + Rails.application.reload_routes! + end + + it "does not route to login" do + expect({ + get: "/account/login", + }).to_not route_to( + controller: "alchemy/sessions", + action: "new", + ) + end + + it "does not route to create session" do + expect({ + post: "/account/login", + }).to_not route_to( + controller: "alchemy/sessions", + action: "create", + ) + end + + it "does not route to logout" do + expect({ + delete: "/account/logout", + }).to_not route_to( + controller: "alchemy/sessions", + action: "destroy", + ) + end end end