-
+
-
+
Er zijn geen gebruikers om weer te geven
@@ -81,7 +81,7 @@ export default {
computed: {
total: function() {
return this.users.map(user => user.credit)
- .reduce((current, credit) => parseFloat(current) + parseFloat(credit));
+ .reduce((current, credit) => parseFloat(current) + parseFloat(credit), 0);
},
sortedUsers: function() {
diff --git a/app/javascript/packs/users.js b/app/javascript/packs/users.js
index 8d958d8b7..cbaa7f041 100644
--- a/app/javascript/packs/users.js
+++ b/app/javascript/packs/users.js
@@ -11,14 +11,18 @@ document.addEventListener('turbolinks:load', () => {
var element = document.getElementById('users-index');
if (element !== null) {
var manual_users = JSON.parse(element.dataset.manualUsers);
+ var sofia_account_users = JSON.parse(element.dataset.sofiaAccountUsers);
var amber_users = JSON.parse(element.dataset.amberUsers);
- var inactive_users = JSON.parse(element.dataset.inactiveUsers);
+ var not_activated_users = JSON.parse(element.dataset.notActivatedUsers);
+ var deactivated_users = JSON.parse(element.dataset.deactivatedUsers);
new Vue({
el: element,
data: () => ({
manual_users,
+ sofia_account_users,
amber_users,
- inactive_users
+ not_activated_users,
+ deactivated_users
}),
components: {
UsersTable
diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb
new file mode 100644
index 000000000..70f076b30
--- /dev/null
+++ b/app/mailers/user_mailer.rb
@@ -0,0 +1,28 @@
+class UserMailer < ApplicationMailer
+ def account_creation_email(user)
+ @user = user
+ @activate_account_url = SofiaAccount.activate_account_url(@user.id, @user.activation_token)
+ @new_activation_link_url = SofiaAccount.new_activation_link_url(@user.id)
+ @header_text = "Welkom op #{Rails.application.config.x.site_name}!"
+ @call_to_action = { text: 'Klik hier om uw account te activeren!', url: @activate_account_url }
+ mail to: user.email, subject: 'Accountactivatie voor het streepsysteem van uw vereniging'
+ end
+
+ def new_activation_link_email(user)
+ @user = user
+ @activate_account_url = SofiaAccount.activate_account_url(@user.id, @user.activation_token)
+ @new_activation_link_url = SofiaAccount.new_activation_link_url(@user.id)
+ @header_text = "Welkom op #{Rails.application.config.x.site_name}!"
+ @call_to_action = { text: 'Klik hier om uw account te activeren!', url: @activate_account_url }
+ mail to: user.email, subject: 'Accountactivatie voor het streepsysteem van uw vereniging'
+ end
+
+ def forgot_password_email(user)
+ @user = user
+ @username = user.sofia_account.username
+ @reset_password_url = user.sofia_account.reset_password_url(@user.activation_token)
+ @forgot_password_url = SofiaAccount.forgot_password_url
+ @call_to_action = { text: 'Wachtwoord herstellen!', url: @reset_password_url }
+ mail to: user.email, subject: 'Wachtwoordherstel voor het streepsysteem van uw vereniging'
+ end
+end
diff --git a/app/models/role.rb b/app/models/role.rb
index db7220447..f10d157e0 100644
--- a/app/models/role.rb
+++ b/app/models/role.rb
@@ -1,7 +1,7 @@
class Role < ApplicationRecord
enum role_type: { treasurer: 0, main_bartender: 1, renting_manager: 2 }
- validates :role_type, :group_uid, presence: true
+ validates :role_type, presence: true
has_many :roles_users, class_name: 'RolesUsers', dependent: :destroy
has_many :users, through: :roles_users
diff --git a/app/models/sofia_account.rb b/app/models/sofia_account.rb
new file mode 100644
index 000000000..8d25823a1
--- /dev/null
+++ b/app/models/sofia_account.rb
@@ -0,0 +1,35 @@
+class SofiaAccount < OmniAuth::Identity::Models::ActiveRecord
+ has_one_time_password
+
+ belongs_to :user
+
+ validates :user, uniqueness: true
+ validates :username, presence: true, uniqueness: true
+ # the presence of :password is already checked by omniauth-sofia-account itself
+ validates :password, length: { minimum: 12 }, allow_nil: true
+
+ auth_key :username # specifies the field within the model that will be used during the login process as username
+
+ def self.activate_account_url(user_id, activation_token)
+ params = { user_id:, activation_token: }
+ default_options = Rails.application.config.action_mailer.default_url_options
+ URI::Generic.build(default_options.merge(path: '/sofia_accounts/activate_account', query: params.to_query)).to_s
+ end
+
+ def self.new_activation_link_url(user_id)
+ params = { user_id: }
+ default_options = Rails.application.config.action_mailer.default_url_options
+ URI::Generic.build(default_options.merge(path: '/sofia_accounts/new_activation_link', query: params.to_query)).to_s
+ end
+
+ def self.forgot_password_url
+ default_options = Rails.application.config.action_mailer.default_url_options
+ URI::Generic.build(default_options.merge(path: '/sofia_accounts/forgot_password')).to_s
+ end
+
+ def reset_password_url(activation_token)
+ params = { activation_token: }
+ default_options = Rails.application.config.action_mailer.default_url_options
+ URI::Generic.build(default_options.merge(path: "/sofia_accounts/#{id}/reset_password", query: params.to_query)).to_s
+ end
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index 7eece9722..6bf77a2b5 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -1,5 +1,5 @@
class User < ApplicationRecord # rubocop:disable Metrics/ClassLength
- devise :omniauthable, omniauth_providers: [:amber_oauth2]
+ devise :omniauthable, omniauth_providers: %i[amber_oauth2 identity]
has_many :orders, dependent: :destroy
has_many :order_rows, through: :orders, dependent: :destroy
has_many :credit_mutations, dependent: :destroy
@@ -11,15 +11,41 @@ class User < ApplicationRecord # rubocop:disable Metrics/ClassLength
validates :name, presence: true
validates :uid, uniqueness: true, allow_blank: true
validate :no_deactivation_when_nonzero_credit
+ validates :email, format: { with: Devise.email_regexp }, allow_blank: true
+ validates :email, presence: true, if: ->(user) { !user.deactivated && user.sofia_account.present? }
scope :in_amber, -> { where(provider: 'amber_oauth2') }
+ scope :sofia_account, -> { where(provider: 'sofia_account') }
scope :manual, -> { where(provider: nil) }
- scope :active, -> { where(deactivated: false) }
- scope :inactive, -> { where(deactivated: true) }
+ scope :active, (lambda {
+ where(deactivated: false).where('(provider IS NULL OR provider != ?) OR
+ (provider = ? AND id IN (?))', 'sofia_account', 'sofia_account', SofiaAccount.select('user_id'))
+ })
+ scope :not_activated, -> { where(deactivated: false, provider: 'sofia_account').where.not(id: SofiaAccount.select('user_id')) }
+ scope :deactivated, -> { where(deactivated: true) }
scope :treasurer, -> { joins(:roles).merge(Role.treasurer) }
+ has_one :sofia_account, dependent: :destroy
+ accepts_nested_attributes_for :sofia_account
+
attr_accessor :current_activity
+ before_save do
+ if new_record? && provider == 'sofia_account'
+ self.activation_token = SecureRandom.urlsafe_base64
+ self.activation_token_valid_till = 5.days.from_now
+ end
+ end
+
+ after_save do
+ age
+ archive! if deactivated && (new_record? || deactivated_previously_changed?(from: false, to: true))
+ end
+
+ after_create do
+ UserMailer.account_creation_email(self).deliver_later if User.sofia_account.exists?(id:)
+ end
+
def credit
credit_mutations.sum('amount') - order_rows.sum('product_count * price_per_product')
end
@@ -70,6 +96,8 @@ def renting_manager?
end
def update_role(groups)
+ return unless User.in_amber.exists?(id)
+
roles_to_have = Role.where(group_uid: groups)
roles_users_to_have = roles_to_have.map { |role| RolesUsers.find_or_create_by(role:, user: self) }
@@ -79,7 +107,7 @@ def update_role(groups)
def archive!
attributes.each_key do |attribute|
- self[attribute] = nil unless %w[deleted_at updated_at created_at provider id uid].include? attribute
+ self[attribute] = nil unless %w[deleted_at updated_at created_at provider sofia_account id uid].include? attribute
end
self.name = "Gearchiveerde gebruiker #{id}"
self.deactivated = true
@@ -102,6 +130,11 @@ def self.from_omniauth(auth) # rubocop:disable Metrics/AbcSize, Metrics/MethodLe
user
end
+ def self.from_omniauth_inspect(auth)
+ sofia_account = SofiaAccount.find(auth.uid)
+ sofia_account.user
+ end
+
# :nocov:
def self.full_name_from_attributes(first_name, last_name_prefix, last_name, nickname)
diff --git a/app/policies/sofia_account_policy.rb b/app/policies/sofia_account_policy.rb
new file mode 100644
index 000000000..73172e93e
--- /dev/null
+++ b/app/policies/sofia_account_policy.rb
@@ -0,0 +1,21 @@
+class SofiaAccountPolicy < ApplicationPolicy
+ def update?
+ record.user == user && User.exists?(id: record.user)
+ end
+
+ def update_with_sofia_account?
+ update?
+ end
+
+ def update_password?
+ update?
+ end
+
+ def enable_otp?
+ update?
+ end
+
+ def disable_otp?
+ update?
+ end
+end
diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb
index 98a09333d..ea05fb35a 100644
--- a/app/policies/user_policy.rb
+++ b/app/policies/user_policy.rb
@@ -22,4 +22,8 @@ def json?
def activities?
show?
end
+
+ def update_with_sofia_account?
+ record == user && User.active.sofia_account.exists?(id: record)
+ end
end
diff --git a/app/views/partials/_flash.html.erb b/app/views/partials/_flash.html.erb
index 1de88aca9..9f2c49dd3 100644
--- a/app/views/partials/_flash.html.erb
+++ b/app/views/partials/_flash.html.erb
@@ -1,12 +1,12 @@
<% if flash.any? %>
-
+
<% flash.each do |key, value| %>
- <%= value %>
+ <%= value.html_safe %>
diff --git a/app/views/partials/_login_prompt.html.erb b/app/views/partials/_login_prompt.html.erb
index 0ba91def2..d27dcaaf4 100644
--- a/app/views/partials/_login_prompt.html.erb
+++ b/app/views/partials/_login_prompt.html.erb
@@ -2,16 +2,31 @@
-
-
- Log in with a <%= Rails.application.config.x.site_association %> account.
-
-
- <%= link_to("Sign in with #{Rails.application.config.x.site_association}", user_amber_oauth2_omniauth_authorize_path, class: 'btn btn-primary', method: :post) %>
-
-
+
+ <% if Rails.application.config.x.site_association == 'C.S.V. Alpha' %>
+
+
+ Log in met een <%= Rails.application.config.x.site_association %> account.
+
+
+ <%= link_to("Log in met #{Rails.application.config.x.site_association}", user_amber_oauth2_omniauth_authorize_path, class: 'btn btn-primary', method: :post) %>
+
+
+
+ OF
+
+ <% end %>
+
+
+ Log in met een streepsysteem account.
+
+
+ <%= link_to "Log in", login_sofia_accounts_path, class: 'btn btn-primary'%>
+
+
+
diff --git a/app/views/sofia_accounts/activate_account.html.erb b/app/views/sofia_accounts/activate_account.html.erb
new file mode 100644
index 000000000..c19e1f42c
--- /dev/null
+++ b/app/views/sofia_accounts/activate_account.html.erb
@@ -0,0 +1,20 @@
+<% content_for :title, "Account activatie - #{Rails.application.config.x.site_name}" %>
+
+
+
+
+ <%= simple_form_for @sofia_account, url: sofia_accounts_path(user_id: @user_id, activation_token: @activation_token), method: :post, wrapper: :vertical_form do |f| %>
+ <%= f.input :username, as: :string, required: true, input_html: {class: 'sofia-account-input'} %>
+ <% if @request_email %>
+ <%= f.input :email, as: :email, required: true, input_html: {class: 'sofia-account-input', name: 'user[email]', value: ''} %>
+ <% end %>
+ <%= f.input :password, as: :password, required: true, input_html: {class: 'sofia-account-input'} %>
+ <%= f.input :password_confirmation, as: :password, required: true, input_html: {class: 'sofia-account-input'} %>
+
+ <%= f.button :submit, 'Activeren', class: 'btn btn-primary' %>
+
+ <% end %>
+
+
\ No newline at end of file
diff --git a/app/views/sofia_accounts/forgot_password_view.html.erb b/app/views/sofia_accounts/forgot_password_view.html.erb
new file mode 100644
index 000000000..b02dc4cb1
--- /dev/null
+++ b/app/views/sofia_accounts/forgot_password_view.html.erb
@@ -0,0 +1,16 @@
+<% content_for :title, "Wachtwoord vergeten - #{Rails.application.config.x.site_name}" %>
+
+
+
+
+ <%= simple_form_for :sofia_account, url: forgot_password_sofia_accounts_path, method: :post, wrapper: :vertical_form do |f| %>
+
Door dit formulier in te vullen ontvangt u een email met een link om een nieuw wachwoord in te stellen.
+ <%= f.input :username, as: :string, label: "Gebruikersnaam", input_html: {name: 'username', class: 'sofia-account-input'} %>
+
+ <%= f.button :submit, 'Link aanvragen', method: :post, class: 'btn btn-primary mt-1' %>
+
+ <% end %>
+
+
diff --git a/app/views/sofia_accounts/login.html.erb b/app/views/sofia_accounts/login.html.erb
new file mode 100644
index 000000000..011e46472
--- /dev/null
+++ b/app/views/sofia_accounts/login.html.erb
@@ -0,0 +1,107 @@
+<% content_for :title, "Login - #{Rails.application.config.x.site_name}" %>
+
+
+
+<%= simple_form_for :sofia_account, id: "authentication_form", authenticity_token: false, wrapper: :vertical_form do |f| %>
+
+
+
+ <%= f.input :auth_key, as: :string, label: "Gebruikersnaam", input_html: {name: 'auth_key', class: 'sofia-account-input'} %>
+ <%= f.input :password, as: :password, label: "Wachtwoord", hint: 'Wachtwoord vergeten?', wrapper: :with_hint_link, input_html: {name: 'password', class: 'sofia-account-input'}, hint_html: {href: forgot_password_view_sofia_accounts_path} %>
+
+ Inloggen
+
+
+
+
+
+
+
+ <%= f.input :verification_code, as: :string, label: "Authenticatiecode", input_html: {name: 'verification_code', value: '', class: 'sofia-account-input'} %>
+
+ Open de authenticatie app op je telefoon en geef hier de zescijferige authenticatiecode op. Authenticatie codes kwijtgeraakt? Neem dan contact op met de ICT-commissie voor identificatie.
+
+
+ Authenticeren
+
+
+
+<% end %>
+
+
\ No newline at end of file
diff --git a/app/views/sofia_accounts/new_activation_link.html.erb b/app/views/sofia_accounts/new_activation_link.html.erb
new file mode 100644
index 000000000..e3d2a2e9b
--- /dev/null
+++ b/app/views/sofia_accounts/new_activation_link.html.erb
@@ -0,0 +1,10 @@
+<% content_for :title, "Nieuwe activatielink - #{Rails.application.config.x.site_name}" %>
+
+
\ No newline at end of file
diff --git a/app/views/sofia_accounts/reset_password_view.html.erb b/app/views/sofia_accounts/reset_password_view.html.erb
new file mode 100644
index 000000000..0dd72c3b3
--- /dev/null
+++ b/app/views/sofia_accounts/reset_password_view.html.erb
@@ -0,0 +1,16 @@
+<% content_for :title, "Wachtwoord resetten - #{Rails.application.config.x.site_name}" %>
+
+
+
+
+ <%= simple_form_for @sofia_account, url: reset_password_sofia_account_path(activation_token: @activation_token), method: :patch, wrapper: :vertical_form do |f| %>
+ <%= f.input :password, as: :password, required: true, input_html: {class: 'sofia-account-input'} %>
+ <%= f.input :password_confirmation, as: :password, required: true, input_html: {class: 'sofia-account-input'} %>
+
+ <%= f.button :submit, 'Opslaan', method: :patch, class: 'btn btn-primary' %>
+
+ <% end %>
+
+
\ No newline at end of file
diff --git a/app/views/user_mailer/account_creation_email.html.erb b/app/views/user_mailer/account_creation_email.html.erb
new file mode 100644
index 000000000..d9001d2b2
--- /dev/null
+++ b/app/views/user_mailer/account_creation_email.html.erb
@@ -0,0 +1,14 @@
+Er is een account voor u aangemaakt op het streepsysteem van uw vereniging. Deze is op dit moment nog niet geactiveerd.
+
+
+Volg deze link om uw account te activeren:
+
+ <%= @activate_account_url %>
+
+
+
+De link is 5 dagen geldig. Mocht de link verlopen zijn, dan kunt u een nieuwe link aanvragen:
+
+ <%= @new_activation_link_url %>
+
+
diff --git a/app/views/user_mailer/forgot_password_email.html.erb b/app/views/user_mailer/forgot_password_email.html.erb
new file mode 100644
index 000000000..d5f33ca0e
--- /dev/null
+++ b/app/views/user_mailer/forgot_password_email.html.erb
@@ -0,0 +1,18 @@
+Er is een aanvraag gedaan tot het resetten van het wachtwoord voor het account verbonden met dit e-mailadres op het streepsysteem van uw vereniging.
+Als u niets hebt aangevraagd kunt u deze e-mail negeren.
+
+
+Volg deze link om uw nieuwe wachtwoord in te stellen:
+
+ <%= @reset_password_url %>
+
+
+
+Uw gebruikersnaam is
<%= @username %> .
+
+
+De link is 1 dag geldig. Mocht de link verlopen zijn, dan kunt u een nieuwe link aanvragen:
+
+ <%= @forgot_password_url %>
+
+
diff --git a/app/views/user_mailer/new_activation_link_email.html.erb b/app/views/user_mailer/new_activation_link_email.html.erb
new file mode 100644
index 000000000..3d9001beb
--- /dev/null
+++ b/app/views/user_mailer/new_activation_link_email.html.erb
@@ -0,0 +1,14 @@
+Er is een aanvraag gedaan voor een nieuwe activatielink voor het account verbonden met dit e-mailadres op het streepsysteem van uw vereniging.
+
+
+Volg deze link om uw account te activeren:
+
+ <%= @activate_account_url %>
+
+
+
+De link is 1 dag geldig. Mocht de link verlopen zijn, dan kunt u een nieuwe link aanvragen:
+
+ <%= @new_activation_link_url %>
+
+
diff --git a/app/views/users/_edit_sofia_account_modal.html.erb b/app/views/users/_edit_sofia_account_modal.html.erb
new file mode 100644
index 000000000..b0e01eb8c
--- /dev/null
+++ b/app/views/users/_edit_sofia_account_modal.html.erb
@@ -0,0 +1,139 @@
+
+
+
+
+
+
+ <% if @sofia_account && policy(@sofia_account).update_with_sofia_account? %>
+
+
+
+ <%= simple_form_for @user, wrapper: :horizontal_form, url: update_with_sofia_account_user_path, method: :patch do |f| %>
+ <%= f.simple_fields_for :sofia_account do |i| %>
+ <%= i.input :username, required: true %>
+ <% end %>
+ <% if current_user.treasurer? %>
+ <%= f.input :name, label: 'Naam', placeholder: 'Naam', required: true %>
+ <% end %>
+ <%= f.input :email, label: 'E-mailadres', placeholder: 'E-mailadres', required: true %>
+ <% if current_user.treasurer? %>
+
+
+ Gedeactiveerd
+
+
+ <%= f.check_box :deactivated %>
+
+
+ <% end %>
+ <%= f.button :submit, 'Opslaan', :data => {:disable_with => 'Bezig...'}, class: 'btn btn-primary float-end', method: :patch %>
+ <% end %>
+
+
+
+
+
+
+ <%= simple_form_for @sofia_account, wrapper: :horizontal_form, url: update_password_sofia_account_path(@sofia_account.id), method: :patch do |f| %>
+ <%= f.input :old_password %>
+ <%= f.input :password %>
+ <%= f.input :password_confirmation, required: false %>
+ <%= f.button :submit, 'Opslaan', :data => {:disable_with => 'Bezig...'}, class: 'btn btn-primary float-end', method: :patch %>
+ <% end %>
+
+
+
+
+
+
+
+ Two-factor-authenticatie geeft een extra beveiligingslaag door bij het
+ inloggen naast je wachtwoord ook om een zescijferige code te vragen. Deze code
+ wordt gegenereerd door een app op je telefoon. Zo heb je naast iets dat je
+ weet (wachtwoord) ook iets nodig dat je hebt (telefoon) om in te kunnen
+ loggen.
+
+
+ <% if @sofia_account.otp_enabled %>
+
+
+
+
+ Je hebt two-factor-authenticatie geactiveerd!
+
+
+ <%= link_to 'Deactiveren', disable_otp_sofia_account_path, class: 'btn btn-danger float-end' %>
+
+
+
+
+ <% else %>
+ <%= simple_form_for @sofia_account, wrapper: :horizontal_form, url: enable_otp_sofia_account_path(@sofia_account.id), method: :patch do |f| %>
+
+ Scan de QR-code hieronder met een authenticatorapp. Er zijn
+ authenticatorapps beschikbaar voor alle mobiele platforms. Hier volgen
+ enkele links naar apps:
+
+
+
+
+ Nadat je de code gescand hebt, bevestig je de set-up door een keer een
+ gegenereerde code in te voeren.
+
+
+ <%= render inline: @svg_qr_code %>
+
+
+ <%= f.input :verification_code, as: :string, input_html: { name: 'verification_code', value: "" } %>
+
+
+ <%= f.button :submit, '2FA Activeren', :data => {:disable_with => 'Bezig...'}, class: 'btn btn-primary float-end', method: :patch %>
+ <% end %>
+ <% end %>
+
+
+ <% end %>
+
+
+
+
+
diff --git a/app/views/users/_new_sofia_account_user_modal.html.erb b/app/views/users/_new_sofia_account_user_modal.html.erb
new file mode 100644
index 000000000..5cc5b1d08
--- /dev/null
+++ b/app/views/users/_new_sofia_account_user_modal.html.erb
@@ -0,0 +1,27 @@
+
+
+
+
+
+ <%= simple_form_for @new_user, wrapper: :horizontal_form do |f| %>
+ <%= f.hidden_field :provider, value: "sofia_account" %>
+
+ <%= f.input :name, label: 'Naam', placeholder: 'Naam', required: true %>
+ <%= f.input :email, label: 'E-mailadres', placeholder: 'E-mailadres', required: true %>
+
+
+
+ <% end %>
+
+
+
diff --git a/app/views/users/index.html.erb b/app/views/users/index.html.erb
index 9617f796c..6b4004b7b 100644
--- a/app/views/users/index.html.erb
+++ b/app/views/users/index.html.erb
@@ -1,12 +1,13 @@
<% content_for :title, "Gebruikers - #{Rails.application.config.x.site_name}" %>
<% content_for :modal do %>
<%= render 'modal' %>
+ <%= render 'new_sofia_account_user_modal' %>
<% end %>
- <% if current_user.treasurer? %>
+ <% if @sofia_account && policy(@sofia_account).update_with_sofia_account? %>
+
+ <%= fa_icon 'sliders', class: 'me-1' %>
+ Instellingen
+
+ <% elsif current_user.treasurer? %>
<%= fa_icon 'pencil', class: 'me-1' %>
Gebruiker wijzigen
@@ -104,7 +110,6 @@
<%= content_tag :div, class: "alert mb-1 alert-#{@user.credit <= 0 ? 'danger' : 'info'}" do %>
<%= @user.credit <= 0 ? 'Je staat rood!' : 'Inleggen' %>
-
<% if Rails.application.config.x.mollie_api_key.present? %>
@@ -113,18 +118,20 @@
Om je saldo aan te vullen kan je zelf geld overmaken.
<% end %>
<%= "Dat kan naar #{Rails.application.config.x.company_iban} t.n.v. #{Rails.application.config.x.company_name}" %>
- <%= 'onder vermelding van je naam en \'Inleg Zatladder\'.' %>
+ <%
+=begin%>
+ <%= 'onder vermelding van je naam en \'Inleg Zatladder\'.' %>
<% if Rails.application.config.x.mollie_api_key.present? %>
<%= link_to add_payments_path do %>
<%= 'Klik hier om je saldo over te maken via iDEAL ' %>
<% end %>
- <% end %>
-
+ <% end %>
+<%
+=end%>
-
-
- <% end %>
+ <% end %>
+
<%= fa_icon 'question-circle' %>
diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb
index ba1ea2a26..c7d21f645 100644
--- a/config/initializers/devise.rb
+++ b/config/initializers/devise.rb
@@ -1,3 +1,5 @@
+require './app/models/sofia_account'
+
Devise.setup do |config|
config.secret_key = Rails.application.secret_key_base
config.mailer_sender = Rails.application.config.x.ict_email
@@ -12,4 +14,17 @@
config.omniauth :amber_oauth2, Rails.application.config.x.amber_client_id,
Rails.application.config.x.amber_client_secret
+ config.omniauth :identity, model: SofiaAccount, fields: %i[username user_id],
+ locate_conditions: ->(req) { { model.auth_key => req.params['auth_key'] } },
+ on_login: lambda { |e|
+ SofiaAccountsController.action(:omniauth_redirect_login).call(e)
+ },
+ on_registration: lambda { |e|
+ SofiaAccountsController.action(:omniauth_redirect_register).call(e)
+ }
+
+ # TODO: EW
+ # OmniAuth.config.on_failure = Proc.new { |env|
+ # OmniAuth::FailureEndpoint.new(env).redirect_to_failure
+ # }
end
diff --git a/config/initializers/simple_form_bootstrap.rb b/config/initializers/simple_form_bootstrap.rb
index 5bb67b543..5681bb251 100644
--- a/config/initializers/simple_form_bootstrap.rb
+++ b/config/initializers/simple_form_bootstrap.rb
@@ -358,6 +358,21 @@
b.use :hint, wrap_with: { class: 'form-text' }
end
+ # small link underneath input
+ config.wrappers :with_hint_link do |b|
+ b.use :html5
+ b.use :placeholder
+ b.optional :maxlength
+ b.optional :minlength
+ b.optional :pattern
+ b.optional :min_max
+ b.optional :readonly
+ b.use :label, class: 'form-label'
+ b.use :input, class: 'form-control', error_class: config.input_field_error_class, valid_class: config.input_field_valid_class
+ b.use :full_error, wrap_with: { class: 'invalid-feedback' }
+ b.use :hint, wrap_with: { tag: :a, class: 'form-text link-secondary' }
+ end
+
# The default wrapper to be used by the FormBuilder.
config.default_wrapper = :vertical_form
diff --git a/config/locales/nl.yml b/config/locales/nl.yml
index 26fd3301c..27709e1cb 100644
--- a/config/locales/nl.yml
+++ b/config/locales/nl.yml
@@ -31,3 +31,15 @@ nl:
views:
pagination:
truncate: "…"
+
+ activerecord:
+ attributes:
+ sofia_account:
+ activation_token: 'Activatie-token'
+ email: 'Emailadres'
+ old_password: 'Oud wachtwoord'
+ password: 'Wachtwoord'
+ password_confirmation: 'Wachtwoord bevestiging'
+ username: 'Gebruikersnaam'
+ user_id: 'Gebruikers-id'
+ verification_code: 'Verificatie code'
diff --git a/config/routes.rb b/config/routes.rb
index 8838a243c..f0e0d1645 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -27,6 +27,7 @@
member do
get :activities
get :json
+ patch :update_with_sofia_account
end
end
@@ -49,6 +50,23 @@
end
end
+ resources :sofia_accounts, only: %i[create] do
+ collection do
+ get :login
+ get :activate_account
+ get :new_activation_link
+ get :forgot_password, as: :forgot_password_view, action: :forgot_password_view
+ post :forgot_password
+ end
+ member do
+ get :reset_password, as: :reset_password_view, action: :reset_password_view
+ patch :reset_password
+ patch :update_password
+ patch :enable_otp
+ patch :disable_otp
+ end
+ end
+
devise_scope :user do
delete 'sign_out', to: 'devise/sessions#destroy', as: :destroy_user_session
end
diff --git a/db/migrate/20240413094147_create_sofia_accounts.rb b/db/migrate/20240413094147_create_sofia_accounts.rb
new file mode 100644
index 000000000..c5b12fc29
--- /dev/null
+++ b/db/migrate/20240413094147_create_sofia_accounts.rb
@@ -0,0 +1,22 @@
+class CreateSofiaAccounts < ActiveRecord::Migration[7.0]
+ def change
+ create_table :sofia_accounts do |t|
+ t.string :username, unique: true, null: false
+ t.string :password_digest, null: false
+ t.references :user, null: false, unique: true
+ t.string :otp_secret_key, null: false
+ t.boolean :otp_enabled, default: false
+
+ t.datetime :deleted_at
+ t.timestamps
+
+ t.index :username, unique: true
+ end
+
+ add_column :users, :activation_token, :string
+ add_column :users, :activation_token_valid_till, :datetime
+
+ remove_index :roles, column: %i[role_type group_uid]
+ change_column_null :roles, :group_uid, true, 999_999
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 26fcbda0c..2d487d870 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,17 +10,17 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.0].define(version: 2024_02_02_124405) do
+ActiveRecord::Schema[7.1].define(version: 2024_04_13_094147) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
create_table "activities", force: :cascade do |t|
t.string "title", null: false
- t.datetime "start_time", null: false
- t.datetime "end_time", null: false
- t.datetime "deleted_at"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime "start_time", precision: nil, null: false
+ t.datetime "end_time", precision: nil, null: false
+ t.datetime "deleted_at", precision: nil
+ t.datetime "created_at", precision: nil, null: false
+ t.datetime "updated_at", precision: nil, null: false
t.bigint "price_list_id"
t.bigint "created_by_id"
t.bigint "locked_by_id"
@@ -34,9 +34,9 @@
t.bigint "user_id", null: false
t.bigint "activity_id"
t.decimal "amount", precision: 8, scale: 2, null: false
- t.datetime "deleted_at"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime "deleted_at", precision: nil
+ t.datetime "created_at", precision: nil, null: false
+ t.datetime "updated_at", precision: nil, null: false
t.bigint "created_by_id"
t.index ["activity_id"], name: "index_credit_mutations_on_activity_id"
t.index ["created_by_id"], name: "index_credit_mutations_on_created_by_id"
@@ -48,7 +48,7 @@
t.string "name", null: false
t.integer "amount", null: false
t.decimal "price", precision: 8, scale: 2, null: false
- t.datetime "deleted_at"
+ t.datetime "deleted_at", precision: nil
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["invoice_id"], name: "index_invoice_rows_on_invoice_id"
@@ -59,9 +59,9 @@
t.bigint "user_id", null: false
t.bigint "activity_id", null: false
t.integer "status", default: 0, null: false
- t.datetime "deleted_at"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime "deleted_at", precision: nil
+ t.datetime "created_at", precision: nil, null: false
+ t.datetime "updated_at", precision: nil, null: false
t.string "email_override"
t.string "name_override"
t.string "token"
@@ -75,18 +75,17 @@
t.bigint "product_id", null: false
t.integer "product_count", null: false
t.decimal "price_per_product", precision: 8, scale: 2, null: false
- t.datetime "deleted_at"
+ t.datetime "deleted_at", precision: nil
t.index ["order_id"], name: "index_order_rows_on_order_id"
t.index ["product_id"], name: "index_order_rows_on_product_id"
end
create_table "orders", force: :cascade do |t|
- t.decimal "order_total", precision: 8, scale: 2
t.bigint "activity_id", null: false
t.bigint "user_id"
- t.datetime "deleted_at"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime "deleted_at", precision: nil
+ t.datetime "created_at", precision: nil, null: false
+ t.datetime "updated_at", precision: nil, null: false
t.bigint "created_by_id"
t.boolean "paid_with_cash", default: false, null: false
t.boolean "paid_with_pin", default: false, null: false
@@ -100,9 +99,9 @@
t.decimal "amount", precision: 8, scale: 2
t.integer "status", default: 0, null: false
t.bigint "user_id"
- t.datetime "deleted_at"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime "deleted_at", precision: nil
+ t.datetime "created_at", precision: nil, null: false
+ t.datetime "updated_at", precision: nil, null: false
t.bigint "invoice_id"
t.integer "lock_version"
t.index ["invoice_id"], name: "index_payments_on_invoice_id"
@@ -111,9 +110,9 @@
create_table "price_lists", force: :cascade do |t|
t.string "name", null: false
- t.datetime "deleted_at"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime "deleted_at", precision: nil
+ t.datetime "created_at", precision: nil, null: false
+ t.datetime "updated_at", precision: nil, null: false
t.datetime "archived_at"
end
@@ -121,9 +120,9 @@
t.bigint "product_id"
t.bigint "price_list_id"
t.decimal "price", precision: 8, scale: 2
- t.datetime "deleted_at"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime "deleted_at", precision: nil
+ t.datetime "created_at", precision: nil, null: false
+ t.datetime "updated_at", precision: nil, null: false
t.index ["price_list_id"], name: "index_product_prices_on_price_list_id"
t.index ["product_id", "price_list_id", "deleted_at"], name: "index_product_prices_on_product_id_and_price_list_id", unique: true
t.index ["product_id"], name: "index_product_prices_on_product_id"
@@ -131,52 +130,67 @@
create_table "products", force: :cascade do |t|
t.string "name", null: false
- t.datetime "deleted_at"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime "deleted_at", precision: nil
+ t.datetime "created_at", precision: nil, null: false
+ t.datetime "updated_at", precision: nil, null: false
t.integer "category", default: 0, null: false
end
create_table "roles", force: :cascade do |t|
- t.integer "group_uid", null: false
- t.datetime "deleted_at"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.integer "group_uid"
+ t.datetime "deleted_at", precision: nil
+ t.datetime "created_at", precision: nil, null: false
+ t.datetime "updated_at", precision: nil, null: false
t.integer "role_type"
- t.index ["role_type", "group_uid"], name: "index_roles_on_role_type_and_group_uid", unique: true
end
create_table "roles_users", force: :cascade do |t|
t.bigint "user_id", null: false
t.bigint "role_id", null: false
- t.datetime "deleted_at"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime "deleted_at", precision: nil
+ t.datetime "created_at", precision: nil, null: false
+ t.datetime "updated_at", precision: nil, null: false
t.index ["role_id"], name: "index_roles_users_on_role_id"
t.index ["user_id", "role_id", "created_at"], name: "index_roles_users_on_user_id_and_role_id_and_created_at", unique: true
t.index ["user_id"], name: "index_roles_users_on_user_id"
end
- create_table "users", force: :cascade do |t|
- t.string "name"
+ create_table "sofia_accounts", force: :cascade do |t|
+ t.string "username", null: false
+ t.string "password_digest", null: false
+ t.bigint "user_id", null: false
+ t.string "otp_secret_key", null: false
+ t.boolean "otp_enabled", default: false
t.datetime "deleted_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.index ["user_id"], name: "index_sofia_accounts_on_user_id"
+ t.index ["username"], name: "index_sofia_accounts_on_username", unique: true
+ end
+
+ create_table "users", force: :cascade do |t|
+ t.datetime "deleted_at", precision: nil
+ t.datetime "created_at", precision: nil, null: false
+ t.datetime "updated_at", precision: nil, null: false
+ t.string "name"
t.string "provider"
t.string "uid"
t.string "avatar_thumb_url"
t.string "email"
t.date "birthday"
t.boolean "deactivated", default: false, null: false
+ t.string "activation_token"
+ t.datetime "activation_token_valid_till"
t.index ["uid"], name: "index_users_on_uid", unique: true
end
create_table "versions", force: :cascade do |t|
- t.string "item_type", null: false
+ t.string "item_type"
+ t.string "{:null=>false}"
t.integer "item_id", null: false
t.string "event", null: false
t.string "whodunnit"
- t.datetime "created_at"
+ t.datetime "created_at", precision: nil
t.jsonb "object"
t.index ["item_type", "item_id"], name: "index_versions_on_item_type_and_item_id"
end
diff --git a/db/seeds.rb b/db/seeds.rb
index 6bf58e4bd..3678b4c9b 100644
--- a/db/seeds.rb
+++ b/db/seeds.rb
@@ -44,4 +44,7 @@
Role.create(role_type: :treasurer, group_uid: 4)
Role.create(role_type: :renting_manager, group_uid: 5)
Role.create(role_type: :main_bartender, group_uid: 6)
+Role.create(role_type: :treasurer)
+Role.create(role_type: :renting_manager)
+Role.create(role_type: :main_bartender)
# rubocop:enable Rails/Output
diff --git a/spec/controllers/callbacks_controller/sofia_account_spec.rb b/spec/controllers/callbacks_controller/sofia_account_spec.rb
new file mode 100644
index 000000000..3d75ef409
--- /dev/null
+++ b/spec/controllers/callbacks_controller/sofia_account_spec.rb
@@ -0,0 +1,150 @@
+require 'rails_helper'
+
+# Disable omni-auth log messages in test report
+OmniAuth.config.logger = Rails.logger
+
+describe 'SofiaAccount login', type: :request do
+ describe 'POST /users/auth/identity/callback' do
+ let(:sofia_account) do
+ create(:sofia_account, password: 'password1234', password_confirmation: 'password1234')
+ end
+ let(:user) { sofia_account.user }
+ let(:request_params) do
+ {
+ auth_key: sofia_account.username,
+ password: sofia_account.password,
+ verification_code: sofia_account.otp_code
+ }
+ end
+ let(:request) do
+ post '/users/auth/identity/callback', params: request_params
+ end
+
+ context 'with non-existent sofia_account' do
+ before do
+ request_params[:auth_key] = 'something_else'
+ request
+ end
+
+ it 'does not log in user' do
+ expect(signed_in?(nil)).to be true
+ end
+
+ it 'sends a json response' do
+ expect(response.content_type).to eq 'application/json; charset=utf-8'
+ expect(response.parsed_body['state']).to eq 'password_prompt'
+ expect(response.parsed_body['error_message']).to eq 'Inloggen mislukt. De ingevulde gegevens zijn incorrect.'
+ end
+ end
+
+ context 'with sofia_account without otp' do
+ context 'when valid login' do
+ before do
+ request
+ end
+
+ it 'logs in user' do
+ expect(signed_in?(user)).to be true
+ end
+
+ it 'sends a json response' do
+ expect(response.content_type).to eq 'application/json; charset=utf-8'
+ expect(response.parsed_body['state']).to eq 'logged_in'
+ expect(response.parsed_body['error_message']).to be_nil
+ expect(response.parsed_body['redirect_url']).to eq user_path(user.id)
+ end
+ end
+
+ context 'when wrong password' do
+ before do
+ request_params[:password] = 'something_else'
+ request
+ end
+
+ it 'does not log in user' do
+ expect(signed_in?(user)).to be false
+ end
+
+ it 'sends a json response' do
+ expect(response.content_type).to eq 'application/json; charset=utf-8'
+ expect(response.parsed_body['state']).to eq 'password_prompt'
+ expect(response.parsed_body['error_message']).to eq 'Inloggen mislukt. De ingevulde gegevens zijn incorrect.'
+ end
+ end
+ end
+
+ context 'with sofia_account with otp' do
+ context 'when valid login' do
+ before do
+ sofia_account.update(otp_enabled: true)
+ request
+ end
+
+ it 'logs in user' do
+ expect(signed_in?(user)).to be true
+ end
+
+ it 'sends a json response' do
+ expect(response.content_type).to eq 'application/json; charset=utf-8'
+ expect(response.parsed_body['state']).to eq 'logged_in'
+ expect(response.parsed_body['error_message']).to be_nil
+ expect(response.parsed_body['redirect_url']).to eq user_path(user.id)
+ end
+ end
+
+ context 'with wrong password' do
+ before do
+ sofia_account.update(otp_enabled: true)
+ request_params[:password] = 'something_else'
+ request
+ end
+
+ it 'does not log in user' do
+ expect(signed_in?(user)).to be false
+ end
+
+ it 'sends a json response' do
+ expect(response.content_type).to eq 'application/json; charset=utf-8'
+ expect(response.parsed_body['state']).to eq 'password_prompt'
+ expect(response.parsed_body['error_message']).to eq 'Inloggen mislukt. De ingevulde gegevens zijn incorrect.'
+ end
+ end
+
+ context 'without otp code' do
+ before do
+ sofia_account.update(otp_enabled: true)
+ request_params[:verification_code] = nil
+ request
+ end
+
+ it 'does not log in user' do
+ expect(signed_in?(user)).to be false
+ end
+
+ it 'sends a json response' do
+ expect(response.content_type).to eq 'application/json; charset=utf-8'
+ expect(response.parsed_body['state']).to eq 'otp_prompt'
+ expect(response.parsed_body['error_message']).to be_nil
+ end
+ end
+
+ context 'with wrong otp code' do
+ before do
+ sofia_account.update(otp_enabled: true)
+ request_params[:verification_code] = 'something_else'
+ request
+ end
+
+ it 'does not log in user' do
+ expect(signed_in?(user)).to be false
+ end
+
+ it 'sends a json response' do
+ expect(response.content_type).to eq 'application/json; charset=utf-8'
+ expect(response.parsed_body['state']).to eq 'otp_prompt'
+ expect(response.parsed_body['error_message']).to eq 'Inloggen mislukt. De authenticatiecode is incorrect.'
+ end
+ end
+ end
+ end
+end
diff --git a/spec/controllers/sofia_accounts_controller/activate_account_spec.rb b/spec/controllers/sofia_accounts_controller/activate_account_spec.rb
new file mode 100644
index 000000000..0d55c72ff
--- /dev/null
+++ b/spec/controllers/sofia_accounts_controller/activate_account_spec.rb
@@ -0,0 +1,65 @@
+require 'rails_helper'
+
+describe SofiaAccountsController, type: :controller do
+ describe 'GET activate_account' do
+ let(:user) do
+ create(:user, :sofia_account, activation_token: SecureRandom.urlsafe_base64, activation_token_valid_till: 5.days.from_now)
+ end
+ let(:request_params) do
+ {
+ activation_token: user.activation_token,
+ user_id: user.id
+ }
+ end
+ let(:request) do
+ get :activate_account, params: request_params
+ end
+
+ context 'without request_email for user with email' do
+ before do
+ request
+ end
+
+ it 'request_email to be false' do
+ expect(assigns(:request_email)).to be false
+ end
+
+ it { expect(request.status).to eq 200 }
+ end
+
+ context 'without email' do
+ before do
+ user.update(email: nil)
+ request
+ end
+
+ it 'request_email to be true' do
+ expect(assigns(:request_email)).to be true
+ end
+
+ it { expect(request.status).to eq 200 }
+ end
+
+ context 'without user_id' do
+ before do
+ request_params = {
+ activation_token: user.activation_token
+ }
+ request
+ end
+
+ it { expect(request.status).to eq 200 }
+ end
+
+ context 'without activation_token' do
+ before do
+ request_params = {
+ user_id: user.id
+ }
+ request
+ end
+
+ it { expect(request.status).to eq 200 }
+ end
+ end
+end
diff --git a/spec/controllers/sofia_accounts_controller/create_spec.rb b/spec/controllers/sofia_accounts_controller/create_spec.rb
new file mode 100644
index 000000000..832119b2f
--- /dev/null
+++ b/spec/controllers/sofia_accounts_controller/create_spec.rb
@@ -0,0 +1,339 @@
+require 'rails_helper'
+
+describe SofiaAccountsController, type: :controller do
+ describe 'POST create' do
+ let(:user) do
+ create(:user, :sofia_account, activation_token: SecureRandom.urlsafe_base64, activation_token_valid_till: 5.days.from_now)
+ end
+ # we need attrs, because omni-auth identity immediately transforms password into password_digest when building
+ let(:request_params) do
+ {
+ sofia_account: {
+ username: Faker::Internet.username,
+ password: 'password1234',
+ password_confirmation: 'password1234'
+ },
+ activation_token: user.activation_token,
+ user_id: user.id
+ }
+ end
+ let(:request) do
+ post :create, params: request_params
+ end
+
+ context 'without email for user with email' do
+ let(:old_email) { user.email }
+
+ before do
+ old_email
+ request
+ user.reload
+ end
+
+ it 'creates a new sofia_account and updates user' do
+ expect(SofiaAccount.count).to eq 1
+ expect(user.sofia_account).not_to be_nil
+ expect(user.activation_token).to be_nil
+ expect(user.activation_token_valid_till).to be_nil
+ expect(user.email).to eq old_email
+ end
+
+ it 'redirects after create' do
+ expect(response).to be_redirect
+ end
+ end
+
+ context 'with email for user without email' do
+ before do
+ user.update(email: nil)
+ request_params[:user] = { email: Faker::Internet.email }
+ request
+ user.reload
+ end
+
+ it 'creates a new sofia_account and updates user' do
+ expect(SofiaAccount.count).to eq 1
+ expect(user.sofia_account).not_to be_nil
+ expect(user.activation_token).to be_nil
+ expect(user.activation_token_valid_till).to be_nil
+ expect(user.email).to eq request_params[:user][:email]
+ end
+
+ it 'redirects after create' do
+ request
+ expect(response).to be_redirect
+ end
+ end
+
+ context 'with email for user with email' do
+ let(:old_user) { user.dup }
+
+ before do
+ old_user
+ request_params[:user] = { email: Faker::Internet.email }
+ request
+ user.reload
+ end
+
+ it 'creates no new sofia_account and does not update user' do
+ expect(SofiaAccount.count).to eq 0
+ expect(user.sofia_account).to be_nil
+ expect(user.dup.attributes).to eq old_user.attributes
+ expect(flash[:error]).to match(/u heeft al een e-mailadres/)
+ end
+ end
+
+ context 'without email for user without email' do
+ let(:old_user) { user.dup }
+
+ before do
+ user.update(email: nil)
+ old_user
+ request
+ user.reload
+ end
+
+ it 'creates no new sofia_account and does not update user' do
+ expect(SofiaAccount.count).to eq 0
+ expect(user.sofia_account).to be_nil
+ expect(user.dup.attributes).to eq old_user.attributes
+ expect(flash[:error]).to match(/e-mailadres moet opgegeven zijn/)
+ end
+ end
+
+ context 'with invalid email for user without email' do
+ let(:old_user) { user.dup }
+
+ before do
+ user.update(email: nil)
+ request_params[:user] = { email: 'invalid_email' }
+ old_user
+ request
+ user.reload
+ end
+
+ it 'creates no new sofia_account and does not update user' do
+ expect(SofiaAccount.count).to eq 0
+ expect(user.sofia_account).to be_nil
+ expect(user.dup.attributes).to eq old_user.attributes
+ expect(flash[:error]).to match(/e-mailadres is ongeldig/)
+ end
+ end
+
+ context 'without activation_token' do
+ let(:old_user) { user.dup }
+
+ before do
+ request_params[:activation_token] = nil
+ old_user
+ request
+ user.reload
+ end
+
+ it 'creates no new sofia_account and does not update user' do
+ expect(SofiaAccount.count).to eq 0
+ expect(user.sofia_account).to be_nil
+ expect(user.dup.attributes).to eq old_user.attributes
+ expect(flash[:error]).to match(/activatie-token is niet aanwezig/)
+ end
+ end
+
+ context 'without user_id' do
+ let(:old_user) { user.dup }
+
+ before do
+ request_params[:user_id] = nil
+ old_user
+ request
+ user.reload
+ end
+
+ it 'creates no new sofia_account and does not update user' do
+ expect(SofiaAccount.count).to eq 0
+ expect(user.sofia_account).to be_nil
+ expect(user.dup.attributes).to eq old_user.attributes
+ expect(flash[:error]).to match(/gebruikers-id is niet aanwezig/)
+ end
+ end
+
+ context 'with expired activation_token' do
+ let(:old_user) { user.dup }
+
+ before do
+ user.update(activation_token_valid_till: 1.minute.ago)
+ old_user
+ request
+ user.reload
+ end
+
+ it 'creates no new sofia_account and does not update user' do
+ expect(SofiaAccount.count).to eq 0
+ expect(user.sofia_account).to be_nil
+ expect(user.dup.attributes).to eq old_user.attributes
+ expect(flash[:error]).to match(/de activatielink is verlopen of ongeldig/)
+ end
+ end
+
+ context 'with wrong activation_token' do
+ let(:old_user) { user.dup }
+
+ before do
+ request_params[:activation_token] = SecureRandom.urlsafe_base64
+ old_user
+ request
+ user.reload
+ end
+
+ it 'creates no new sofia_account and does not update user' do
+ expect(SofiaAccount.count).to eq 0
+ expect(user.sofia_account).to be_nil
+ expect(user.dup.attributes).to eq old_user.attributes
+ expect(flash[:error]).to match(/de activatielink is verlopen of ongeldig/)
+ end
+ end
+
+ context 'with user already activated' do
+ let(:old_user) { user.dup }
+
+ before do
+ create(:sofia_account, user:)
+ old_user
+ request
+ user.reload
+ end
+
+ it 'creates no new sofia_account and does not update user' do
+ expect(SofiaAccount.count).to eq 1 # this 1 is for the one that already existed
+ expect(user.dup.attributes).to eq old_user.attributes
+ expect(flash[:error]).to match(/uw account is al geactiveerd/)
+ end
+ end
+
+ context 'without username' do
+ let(:old_user) { user.dup }
+
+ before do
+ request_params[:sofia_account][:username] = nil
+ old_user
+ request
+ user.reload
+ end
+
+ it 'creates no new sofia_account and does not update user' do
+ expect(SofiaAccount.count).to eq 0
+ expect(user.sofia_account).to be_nil
+ expect(user.dup.attributes).to eq old_user.attributes
+ expect(flash[:error]).to match(/gebruikersnaam moet opgegeven zijn/)
+ end
+ end
+
+ context 'without password' do
+ let(:old_user) { user.dup }
+
+ before do
+ request_params[:sofia_account][:password] = nil
+ old_user
+ request
+ user.reload
+ end
+
+ it 'creates no new sofia_account and does not update user' do
+ expect(SofiaAccount.count).to eq 0
+ expect(user.sofia_account).to be_nil
+ expect(user.dup.attributes).to eq old_user.attributes
+ expect(flash[:error]).to match(/wachtwoord moet opgegeven zijn/)
+ end
+ end
+
+ context 'without password_confirmation' do
+ let(:old_user) { user.dup }
+
+ before do
+ request_params[:sofia_account][:password_confirmation] = nil
+ old_user
+ request
+ user.reload
+ end
+
+ it 'creates no new sofia_account and does not update user' do
+ expect(SofiaAccount.count).to eq 0
+ expect(user.sofia_account).to be_nil
+ expect(user.dup.attributes).to eq old_user.attributes
+ expect(flash[:error]).to match(/wachtwoord bevestiging komt niet overeen met wachtwoord/)
+ end
+ end
+
+ context 'with wrong password_confirmation' do
+ let(:old_user) { user.dup }
+
+ before do
+ request_params[:sofia_account][:password_confirmation] = 'something_else'
+ old_user
+ request
+ user.reload
+ end
+
+ it 'creates no new sofia_account and does not update user' do
+ expect(SofiaAccount.count).to eq 0
+ expect(user.sofia_account).to be_nil
+ expect(user.dup.attributes).to eq old_user.attributes
+ expect(flash[:error]).to match(/wachtwoord bevestiging komt niet overeen met wachtwoord/)
+ end
+ end
+
+ context 'with invalid password' do
+ let(:old_user) { user.dup }
+
+ before do
+ request_params[:sofia_account][:password] = 'too_short'
+ request_params[:sofia_account][:password_confirmation] = 'too_short'
+ old_user
+ request
+ user.reload
+ end
+
+ it 'creates no new sofia_account and does not update user' do
+ expect(SofiaAccount.count).to eq 0
+ expect(user.sofia_account).to be_nil
+ expect(user.dup.attributes).to eq old_user.attributes
+ expect(flash[:error]).to match(/wachtwoord is te kort/)
+ end
+ end
+
+ context 'with user_id of non-existent user' do
+ let(:old_user) { user.dup }
+
+ before do
+ request_params[:user_id] = User.count
+ old_user
+ request
+ user.reload
+ end
+
+ it 'creates no new sofia_account and does not update user' do
+ expect(SofiaAccount.count).to eq 0
+ expect(user.sofia_account).to be_nil
+ expect(user.dup.attributes).to eq old_user.attributes
+ expect(flash[:error]).to match(/uw account bestaat niet/)
+ end
+ end
+
+ context 'with user_id of deactivated user' do
+ let(:old_user) { user.dup }
+
+ before do
+ user.update(deactivated: true)
+ old_user
+ request
+ user.reload
+ end
+
+ it 'creates no new sofia_account and does not update user' do
+ expect(SofiaAccount.count).to eq 0
+ expect(user.sofia_account).to be_nil
+ expect(user.dup.attributes).to eq old_user.attributes
+ expect(flash[:error]).to match(/uw account is gedeactiveerd/)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/sofia_accounts_controller/disable_otp_spec.rb b/spec/controllers/sofia_accounts_controller/disable_otp_spec.rb
new file mode 100644
index 000000000..21f861879
--- /dev/null
+++ b/spec/controllers/sofia_accounts_controller/disable_otp_spec.rb
@@ -0,0 +1,64 @@
+require 'rails_helper'
+
+describe SofiaAccountsController, type: :controller do
+ describe 'PATCH disable_otp' do
+ let(:sofia_account) do
+ create(:sofia_account, otp_enabled: true)
+ end
+ let(:request) do
+ patch :disable_otp, params: { id: sofia_account.id }
+ end
+
+ describe 'when as sofia_account owner' do
+ before do
+ sign_in sofia_account.user
+ request
+ sofia_account.reload
+ end
+
+ it 'updates sofia_account' do
+ expect(request.status).to eq 302
+ expect(sofia_account.otp_enabled).to be false
+ end
+ end
+
+ describe 'when as other user' do
+ before do
+ sign_in create(:user)
+ request
+ sofia_account.reload
+ end
+
+ it 'does not update sofia_account' do
+ expect(request.status).to eq 403
+ expect(sofia_account.otp_enabled).to be true
+ end
+ end
+
+ describe 'when as main-bartender' do
+ before do
+ sign_in create(:user, :main_bartender)
+ request
+ sofia_account.reload
+ end
+
+ it 'does not update sofia_account' do
+ expect(request.status).to eq 403
+ expect(sofia_account.otp_enabled).to be true
+ end
+ end
+
+ describe 'when as treasurer' do
+ before do
+ sign_in create(:user, :treasurer)
+ request
+ sofia_account.reload
+ end
+
+ it 'does not update sofia_account' do
+ expect(request.status).to eq 403
+ expect(sofia_account.otp_enabled).to be true
+ end
+ end
+ end
+end
diff --git a/spec/controllers/sofia_accounts_controller/enable_otp_spec.rb b/spec/controllers/sofia_accounts_controller/enable_otp_spec.rb
new file mode 100644
index 000000000..598cdbff2
--- /dev/null
+++ b/spec/controllers/sofia_accounts_controller/enable_otp_spec.rb
@@ -0,0 +1,101 @@
+require 'rails_helper'
+
+describe SofiaAccountsController, type: :controller do
+ describe 'PATCH enable_otp' do
+ let(:sofia_account) do
+ create(:sofia_account)
+ end
+ let(:user) { sofia_account.user }
+ let(:request_params) do
+ {
+ id: sofia_account.id,
+ verification_code: sofia_account.otp_code
+ }
+ end
+ let(:request) do
+ patch :enable_otp, params: request_params
+ end
+
+ describe 'when as sofia_account owner' do
+ before do
+ sign_in user
+ request
+ sofia_account.reload
+ end
+
+ it 'updates sofia_account' do
+ expect(request.status).to eq 302
+ expect(sofia_account.otp_enabled).to be true
+ end
+ end
+
+ describe 'when as other user' do
+ before do
+ sign_in create(:user)
+ request
+ sofia_account.reload
+ end
+
+ it 'does not update sofia_account' do
+ expect(request.status).to eq 403
+ expect(sofia_account.otp_enabled).to be false
+ end
+ end
+
+ describe 'when as main-bartender' do
+ before do
+ sign_in create(:user, :main_bartender)
+ request
+ sofia_account.reload
+ end
+
+ it 'does not update sofia_account' do
+ expect(request.status).to eq 403
+ expect(sofia_account.otp_enabled).to be false
+ end
+ end
+
+ describe 'when as treasurer' do
+ before do
+ sign_in create(:user, :treasurer)
+ request
+ sofia_account.reload
+ end
+
+ it 'does not update sofia_account' do
+ expect(request.status).to eq 403
+ expect(sofia_account.otp_enabled).to be false
+ end
+ end
+
+ describe 'without verification_code' do
+ before do
+ request_params[:verification_code] = nil
+ sign_in user
+ request
+ sofia_account.reload
+ end
+
+ it 'does not update sofia_account' do
+ expect(request.status).to eq 302
+ expect(sofia_account.otp_enabled).to be false
+ expect(flash[:error]).to match(/de verificatie token is niet aanwezig/)
+ end
+ end
+
+ describe 'with wrong verication_code' do
+ before do
+ request_params[:verification_code] = SecureRandom.urlsafe_base64
+ sign_in user
+ request
+ sofia_account.reload
+ end
+
+ it 'does not update sofia_account' do
+ expect(request.status).to eq 302
+ expect(sofia_account.otp_enabled).to be false
+ expect(flash[:error]).to match(/de verificatie token is ongeldig/)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/sofia_accounts_controller/forgot_pasword_spec.rb b/spec/controllers/sofia_accounts_controller/forgot_pasword_spec.rb
new file mode 100644
index 000000000..6de3156ac
--- /dev/null
+++ b/spec/controllers/sofia_accounts_controller/forgot_pasword_spec.rb
@@ -0,0 +1,89 @@
+require 'rails_helper'
+
+describe SofiaAccountsController, type: :controller do
+ describe 'POST forgot_password' do
+ let(:user) { create(:user, :sofia_account) }
+ let(:sofia_account) { create(:sofia_account, user:) }
+ # we need attrs, because omni-auth identity immediately transforms password into password_digest when building
+ let(:request_params) do
+ {
+ username: sofia_account.username
+ }
+ end
+ let(:request) do
+ post :forgot_password, params: request_params
+ end
+
+ context 'when valid' do
+ before do
+ clear_enqueued_jobs
+ end
+
+ after do
+ clear_enqueued_jobs
+ end
+
+ it 'shows success message and sends email' do
+ request
+ user.reload
+ expect(UserMailer).to send_email(:forgot_password_email, :deliver_later, user)
+ expect(user.activation_token).not_to be_nil
+ expect(user.activation_token_valid_till).not_to be_nil
+ end
+ end
+
+ context 'without email' do
+ let(:old_user) { user.dup }
+
+ before do
+ user.update(email: nil)
+ old_user
+ clear_enqueued_jobs
+ request
+ user.reload
+ end
+
+ it 'shows error message and does not send email' do
+ expect(user.dup.attributes).to eq old_user.attributes
+ expect(flash[:error]).to match(/uw account heeft geen emailadres/)
+ expect(enqueued_jobs.size).to eq(0)
+ end
+ end
+
+ context 'without username' do
+ let(:old_user) { user.dup }
+
+ before do
+ request_params[:username] = nil
+ old_user
+ clear_enqueued_jobs
+ request
+ user.reload
+ end
+
+ it 'shows error message and does not send email' do
+ expect(user.dup.attributes).to eq old_user.attributes
+ expect(flash[:error]).to match(/gebruikersnaam is niet aanwezig/)
+ expect(enqueued_jobs.size).to eq(0)
+ end
+ end
+
+ context 'with non-existent username' do
+ let(:old_user) { user.dup }
+
+ before do
+ request_params[:username] = 'something_else'
+ old_user
+ clear_enqueued_jobs
+ request
+ user.reload
+ end
+
+ it 'shows error message and does not send email' do
+ expect(user.dup.attributes).to eq old_user.attributes
+ expect(flash[:error]).to match(/gebruikersnaam bestaat niet/)
+ expect(enqueued_jobs.size).to eq(0)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/sofia_accounts_controller/new_activation_link_spec.rb b/spec/controllers/sofia_accounts_controller/new_activation_link_spec.rb
new file mode 100644
index 000000000..e9d460089
--- /dev/null
+++ b/spec/controllers/sofia_accounts_controller/new_activation_link_spec.rb
@@ -0,0 +1,122 @@
+require 'rails_helper'
+
+describe SofiaAccountsController, type: :controller do
+ describe 'GET new_activation_link' do
+ let(:user) do
+ create(:user, :sofia_account, activation_token: SecureRandom.urlsafe_base64, activation_token_valid_till: 5.days.from_now)
+ end
+ let(:request_params) do
+ {
+ activation_token: user.activation_token,
+ user_id: user.id
+ }
+ end
+ let(:request) do
+ get :new_activation_link, params: request_params
+ end
+
+ context 'when valid' do
+ before do
+ clear_enqueued_jobs
+ end
+
+ after do
+ clear_enqueued_jobs
+ end
+
+ it 'shows success message and sends email' do
+ request
+ expect(assigns(:message)).to eq 'Er is een nieuwe activatielink voor uw account verstuurd naar uw emailadres.'
+ expect(UserMailer).to send_email(:new_activation_link_email, :deliver_later, user)
+ user.reload
+ expect(user.activation_token).not_to be_nil
+ expect(user.activation_token_valid_till).not_to be_nil
+ end
+ end
+
+ context 'without email' do
+ let(:old_user) { user.dup }
+
+ before do
+ user.update(email: nil)
+ old_user
+ clear_enqueued_jobs
+ request
+ end
+
+ it 'shows error message and does not send email' do
+ expect(user.dup.attributes).to eq old_user.attributes
+ expect(assigns(:message)).to match(/uw account heeft geen emailadres/)
+ expect(enqueued_jobs.size).to eq(0)
+ end
+ end
+
+ context 'without user_id' do
+ let(:old_user) { user.dup }
+
+ before do
+ request_params[:user_id] = nil
+ old_user
+ clear_enqueued_jobs
+ request
+ end
+
+ it 'shows error message and does not send email' do
+ expect(user.dup.attributes).to eq old_user.attributes
+ expect(assigns(:message)).to match(/gebruikers-id is niet aanwezig/)
+ expect(enqueued_jobs.size).to eq(0)
+ end
+ end
+
+ context 'with user already activated' do
+ let(:old_user) { user.dup }
+
+ before do
+ user.update(sofia_account: create(:sofia_account, user:))
+ old_user
+ clear_enqueued_jobs
+ request
+ end
+
+ it 'shows error message and does not send email' do
+ expect(user.dup.attributes).to eq old_user.attributes
+ expect(assigns(:message)).to match(/uw account is al geactiveerd/)
+ expect(enqueued_jobs.size).to eq(0)
+ end
+ end
+
+ context 'with user_id of non-existent user' do
+ let(:old_user) { user.dup }
+
+ before do
+ request_params[:user_id] = User.count + 1
+ old_user
+ clear_enqueued_jobs
+ request
+ end
+
+ it 'shows error message and does not send email' do
+ expect(user.dup.attributes).to eq old_user.attributes
+ expect(assigns(:message)).to match(/uw account bestaat niet/)
+ expect(enqueued_jobs.size).to eq(0)
+ end
+ end
+
+ context 'with user_id of deactivated user' do
+ let(:old_user) { user.dup }
+
+ before do
+ user.update(deactivated: true)
+ old_user
+ clear_enqueued_jobs
+ request
+ end
+
+ it 'shows error message and does not send email' do
+ expect(user.dup.attributes).to eq old_user.attributes
+ expect(assigns(:message)).to match(/uw account is gedeactiveerd/)
+ expect(enqueued_jobs.size).to eq(0)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/sofia_accounts_controller/reset_password_spec.rb b/spec/controllers/sofia_accounts_controller/reset_password_spec.rb
new file mode 100644
index 000000000..6704f502c
--- /dev/null
+++ b/spec/controllers/sofia_accounts_controller/reset_password_spec.rb
@@ -0,0 +1,177 @@
+require 'rails_helper'
+
+describe SofiaAccountsController, type: :controller do
+ describe 'PATCH reset_password' do
+ let(:user) do
+ create(:user, :sofia_account, activation_token: SecureRandom.urlsafe_base64, activation_token_valid_till: 5.days.from_now)
+ end
+ let(:sofia_account) do
+ create(:sofia_account, user:, password: 'password1234', password_confirmation: 'password1234')
+ end
+ let(:request_params) do
+ {
+ id: sofia_account.id,
+ sofia_account: {
+ password: 'new_password1234',
+ password_confirmation: 'new_password1234'
+ },
+ activation_token: user.activation_token
+ }
+ end
+ let(:request) do
+ patch :reset_password, params: request_params
+ end
+
+ context 'when valid' do
+ let(:old_sofia_account) { sofia_account.dup }
+
+ before do
+ old_sofia_account
+ request
+ sofia_account.reload
+ user.reload
+ end
+
+ it 'updates sofia_account' do
+ expect(sofia_account.authenticate(old_sofia_account.password)).to be false
+ expect(sofia_account.authenticate(request_params[:sofia_account][:password])).to be sofia_account
+ expect(user.activation_token).to be_nil
+ expect(user.activation_token_valid_till).to be_nil
+ end
+ end
+
+ context 'without activation_token' do
+ let(:old_sofia_account) { sofia_account.dup }
+
+ before do
+ request_params[:activation_token] = nil
+ old_sofia_account
+ request
+ sofia_account.reload
+ user.reload
+ end
+
+ it 'does not update sofia_account' do
+ expect(sofia_account.dup.attributes).to eq old_sofia_account.attributes
+ expect(flash[:error]).to match(/activatie-token is niet aanwezig/)
+ expect(user.activation_token).not_to be_nil
+ expect(user.activation_token_valid_till).not_to be_nil
+ end
+ end
+
+ context 'with expired activation_token' do
+ let(:old_sofia_account) { sofia_account.dup }
+
+ before do
+ user.update(activation_token_valid_till: 1.minute.ago)
+ old_sofia_account
+ request
+ sofia_account.reload
+ user.reload
+ end
+
+ it 'does not update sofia_account' do
+ expect(sofia_account.dup.attributes).to eq old_sofia_account.attributes
+ expect(flash[:error]).to match(/de resetlink is verlopen of ongeldig/)
+ expect(user.activation_token).not_to be_nil
+ expect(user.activation_token_valid_till).not_to be_nil
+ end
+ end
+
+ context 'with wrong activation_token' do
+ let(:old_sofia_account) { sofia_account.dup }
+
+ before do
+ request_params[:activation_token] = SecureRandom.urlsafe_base64
+ old_sofia_account
+ request
+ sofia_account.reload
+ user.reload
+ end
+
+ it 'does not update sofia_account' do
+ expect(sofia_account.dup.attributes).to eq old_sofia_account.attributes
+ expect(flash[:error]).to match(/de resetlink is verlopen of ongeldig/)
+ expect(user.activation_token).not_to be_nil
+ expect(user.activation_token_valid_till).not_to be_nil
+ end
+ end
+
+ context 'without password' do
+ let(:old_sofia_account) { sofia_account.dup }
+
+ before do
+ request_params[:sofia_account][:password] = nil
+ old_sofia_account
+ request
+ sofia_account.reload
+ user.reload
+ end
+
+ it 'does not update sofia_account' do
+ expect(sofia_account.dup.attributes).to eq old_sofia_account.attributes
+ expect(flash[:error]).to match(/wachtwoord moet opgegeven zijn/)
+ expect(user.activation_token).not_to be_nil
+ expect(user.activation_token_valid_till).not_to be_nil
+ end
+ end
+
+ context 'without password_confirmation' do
+ let(:old_sofia_account) { sofia_account.dup }
+
+ before do
+ request_params[:sofia_account][:password_confirmation] = nil
+ old_sofia_account
+ request
+ sofia_account.reload
+ user.reload
+ end
+
+ it 'does not update sofia_account' do
+ expect(sofia_account.dup.attributes).to eq old_sofia_account.attributes
+ expect(flash[:error]).to match(/wachtwoord bevestiging komt niet overeen met wachtwoord/)
+ expect(user.activation_token).not_to be_nil
+ expect(user.activation_token_valid_till).not_to be_nil
+ end
+ end
+
+ context 'with wrong password_confirmation' do
+ let(:old_sofia_account) { sofia_account.dup }
+
+ before do
+ request_params[:sofia_account][:password_confirmation] = 'something_else'
+ old_sofia_account
+ request
+ sofia_account.reload
+ user.reload
+ end
+
+ it 'does not update sofia_account' do
+ expect(sofia_account.dup.attributes).to eq old_sofia_account.attributes
+ expect(flash[:error]).to match(/wachtwoord bevestiging komt niet overeen met wachtwoord/)
+ expect(user.activation_token).not_to be_nil
+ expect(user.activation_token_valid_till).not_to be_nil
+ end
+ end
+
+ context 'with invalid password' do
+ let(:old_sofia_account) { sofia_account.dup }
+
+ before do
+ request_params[:sofia_account][:password] = 'too_short'
+ request_params[:sofia_account][:password_confirmation] = 'too_short'
+ old_sofia_account
+ request
+ sofia_account.reload
+ user.reload
+ end
+
+ it 'does not update sofia_account' do
+ expect(sofia_account.dup.attributes).to eq old_sofia_account.attributes
+ expect(flash[:error]).to match(/wachtwoord is te kort/)
+ expect(user.activation_token).not_to be_nil
+ expect(user.activation_token_valid_till).not_to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/controllers/sofia_accounts_controller/update_password_spec.rb b/spec/controllers/sofia_accounts_controller/update_password_spec.rb
new file mode 100644
index 000000000..2df464fd5
--- /dev/null
+++ b/spec/controllers/sofia_accounts_controller/update_password_spec.rb
@@ -0,0 +1,191 @@
+require 'rails_helper'
+
+describe SofiaAccountsController, type: :controller do
+ describe 'PATCH update_password' do
+ let(:sofia_account) do
+ create(:sofia_account, password: 'password1234', password_confirmation: 'password1234')
+ end
+ let(:user) { sofia_account.user }
+ let(:request_params) do
+ {
+ id: sofia_account.id,
+ sofia_account: {
+ old_password: sofia_account.password,
+ password: 'new_password1234',
+ password_confirmation: 'new_password1234'
+ }
+ }
+ end
+ let(:request) do
+ patch :update_password, params: request_params
+ end
+
+ describe 'when as sofia_account owner' do
+ let(:old_sofia_account) { sofia_account.dup }
+
+ before do
+ old_sofia_account
+ sign_in user
+ request
+ sofia_account.reload
+ end
+
+ it 'updates sofia_account' do
+ expect(request.status).to eq 302
+ expect(sofia_account.authenticate(old_sofia_account.password)).to be false
+ expect(sofia_account.authenticate(request_params[:sofia_account][:password])).to be sofia_account
+ end
+ end
+
+ describe 'when as other user' do
+ let(:old_sofia_account) { sofia_account.dup }
+
+ before do
+ old_sofia_account
+ sign_in create(:user)
+ request
+ sofia_account.reload
+ end
+
+ it 'does not update sofia_account' do
+ expect(request.status).to eq 403
+ expect(sofia_account.dup.attributes).to eq old_sofia_account.attributes
+ end
+ end
+
+ describe 'when as main-bartender' do
+ let(:old_sofia_account) { sofia_account.dup }
+
+ before do
+ old_sofia_account
+ sign_in create(:user, :main_bartender)
+ request
+ sofia_account.reload
+ end
+
+ it 'does not update sofia_account' do
+ expect(request.status).to eq 403
+ expect(sofia_account.dup.attributes).to eq old_sofia_account.attributes
+ end
+ end
+
+ describe 'when as treasurer' do
+ let(:old_sofia_account) { sofia_account.dup }
+
+ before do
+ old_sofia_account
+ sign_in create(:user, :treasurer)
+ request
+ sofia_account.reload
+ end
+
+ it 'does not update sofia_account' do
+ expect(request.status).to eq 403
+ expect(sofia_account.dup.attributes).to eq old_sofia_account.attributes
+ end
+ end
+
+ describe 'without old_password' do
+ let(:old_sofia_account) { sofia_account.dup }
+
+ before do
+ old_sofia_account
+ request_params[:sofia_account][:old_password] = nil
+ sign_in user
+ request
+ sofia_account.reload
+ end
+
+ it 'does not update sofia_account' do
+ expect(sofia_account.dup.attributes).to eq old_sofia_account.attributes
+ expect(flash[:error]).to match(/het oude wachtwoord is fout of niet opgegeven/)
+ end
+ end
+
+ describe 'with wrong old_password' do
+ let(:old_sofia_account) { sofia_account.dup }
+
+ before do
+ old_sofia_account
+ request_params[:sofia_account][:old_password] = 'something_else'
+ sign_in user
+ request
+ sofia_account.reload
+ end
+
+ it 'does not update sofia_account' do
+ expect(sofia_account.dup.attributes).to eq old_sofia_account.attributes
+ expect(flash[:error]).to match(/het oude wachtwoord is fout of niet opgegeven/)
+ end
+ end
+
+ describe 'without password' do
+ let(:old_sofia_account) { sofia_account.dup }
+
+ before do
+ old_sofia_account
+ request_params[:sofia_account][:password] = nil
+ sign_in user
+ request
+ sofia_account.reload
+ end
+
+ it 'does not update sofia_account' do
+ expect(sofia_account.dup.attributes).to eq old_sofia_account.attributes
+ expect(flash[:error]).to match(/wachtwoord moet opgegeven zijn/)
+ end
+ end
+
+ describe 'with invalid password' do
+ let(:old_sofia_account) { sofia_account.dup }
+
+ before do
+ old_sofia_account
+ request_params[:sofia_account][:password] = 'too_short'
+ request_params[:sofia_account][:password_confirmation] = 'too_short'
+ sign_in user
+ request
+ sofia_account.reload
+ end
+
+ it 'does not update sofia_account' do
+ expect(sofia_account.dup.attributes).to eq old_sofia_account.attributes
+ expect(flash[:error]).to match(/wachtwoord is te kort/)
+ end
+ end
+
+ describe 'with non-matching confirmation' do
+ let(:old_sofia_account) { sofia_account.dup }
+
+ before do
+ old_sofia_account
+ request_params[:sofia_account][:password_confirmation] = 'something_else'
+ sign_in user
+ request
+ sofia_account.reload
+ end
+
+ it 'does not update sofia_account' do
+ expect(sofia_account.dup.attributes).to eq old_sofia_account.attributes
+ expect(flash[:error]).to match(/wachtwoord bevestiging komt niet overeen met wachtwoord/)
+ end
+ end
+
+ describe 'without password_confirmation' do
+ let(:old_sofia_account) { sofia_account.dup }
+
+ before do
+ old_sofia_account
+ request_params[:sofia_account][:password_confirmation] = nil
+ sign_in user
+ request
+ sofia_account.reload
+ end
+
+ it 'does not update sofia_account' do
+ expect(sofia_account.dup.attributes).to eq old_sofia_account.attributes
+ expect(flash[:error]).to match(/wachtwoord bevestiging komt niet overeen met wachtwoord/)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/users_controller/index_spec.rb b/spec/controllers/users_controller/index_spec.rb
index 4d2b37dba..b306b63d8 100644
--- a/spec/controllers/users_controller/index_spec.rb
+++ b/spec/controllers/users_controller/index_spec.rb
@@ -5,44 +5,62 @@
let(:alice) { create(:user, :treasurer, :manual) }
let(:bob) { create(:user, :renting_manager, :manual) }
let(:carl) { create(:user, :main_bartender, :manual) }
- let(:amber) { create(:user, :from_amber) }
let(:eve) { create(:user, :manual) }
before do
+ create(:user, :from_amber)
+ create(:user, :sofia_account)
+ active_sofia_account_user = create(:user, :sofia_account)
+ create(:sofia_account, user: active_sofia_account_user)
+ create(:user, :manual, deactivated: true)
alice
bob
carl
- amber
eve
end
context 'when as treasurer' do
- it 'shows all users' do
- sign_in alice
+ before do
+ sign_in carl
get :index
+ end
+ it 'shows all users' do
expect(assigns(:manual_users).size).to eq 4
expect(assigns(:amber_users).size).to eq 1
+ expect(assigns(:sofia_account_users).size).to eq 1
+ expect(assigns(:not_activated_users).size).to eq 1
+ expect(assigns(:deactivated_users).size).to eq 1
end
end
context 'when as renting-manager' do
- it 'shows manual users' do
+ before do
sign_in bob
get :index
+ end
+ it 'shows manual users' do
expect(assigns(:manual_users).size).to eq 4
expect(assigns(:amber_users).size).to eq 0
+ expect(assigns(:sofia_account_users).size).to eq 0
+ expect(assigns(:not_activated_users).size).to eq 0
+ expect(assigns(:deactivated_users).size).to eq 1
end
end
context 'when as main-bartender' do
- it 'shows own user' do
+ before do
sign_in carl
get :index
+ end
+ it 'shows own user' do
expect(assigns(:manual_users).size).to eq 1
expect(assigns(:amber_users).size).to eq 0
+ expect(assigns(:sofia_account_users).size).to eq 0
+ expect(assigns(:not_activated_users).size).to eq 0
+ expect(assigns(:deactivated_users).size).to eq 0
end
end
diff --git a/spec/controllers/users_controller/show_spec.rb b/spec/controllers/users_controller/show_spec.rb
index b1b383987..360c62e57 100644
--- a/spec/controllers/users_controller/show_spec.rb
+++ b/spec/controllers/users_controller/show_spec.rb
@@ -3,10 +3,12 @@
describe UsersController, type: :controller do
describe 'GET show' do
let(:amber) { create(:user, :from_amber) }
+ let(:sofia) { create(:user, :sofia_account) }
let(:eve) { create(:user, :manual) }
before do
amber
+ sofia
eve
end
@@ -24,6 +26,13 @@
expect(response).to have_http_status(:ok)
end
+
+ it 'shows sofia_account user' do
+ sign_in create(:user, :treasurer)
+ get :show, params: { id: sofia.id }
+
+ expect(response).to have_http_status(:ok)
+ end
end
context 'when as renting-manager' do
@@ -40,6 +49,13 @@
expect(response).to have_http_status(:forbidden)
end
+
+ it 'forbids showing sofia_account user' do
+ sign_in create(:user, :renting_manager)
+ get :show, params: { id: sofia.id }
+
+ expect(response).to have_http_status(:forbidden)
+ end
end
context 'when as main-bartender' do
@@ -56,6 +72,13 @@
expect(response).to have_http_status(:forbidden)
end
+
+ it 'forbids showing sofia_account user' do
+ sign_in create(:user, :main_bartender)
+ get :show, params: { id: sofia.id }
+
+ expect(response).to have_http_status(:forbidden)
+ end
end
context 'when as user' do
@@ -72,6 +95,13 @@
expect(response).to have_http_status(:forbidden)
end
+
+ it 'forbids showing sofia_account user' do
+ sign_in create(:user)
+ get :show, params: { id: sofia.id }
+
+ expect(response).to have_http_status(:forbidden)
+ end
end
end
end
diff --git a/spec/controllers/users_controller/update_spec.rb b/spec/controllers/users_controller/update_spec.rb
index 3d3d6a2ef..64d1dbc17 100644
--- a/spec/controllers/users_controller/update_spec.rb
+++ b/spec/controllers/users_controller/update_spec.rb
@@ -10,32 +10,38 @@
end
before do
- sign_in user
+ sign_in action_user
user.name = 'New Name'
request
user.reload
end
- describe 'when as user' do
- let(:user) { create(:user) }
+ describe 'when as user themselves' do
+ let(:action_user) { user }
+
+ it { expect(request.status).to eq 403 }
+ end
+
+ describe 'when as another user' do
+ let(:action_user) { create(:user) }
it { expect(request.status).to eq 403 }
end
describe 'when as main-bartender' do
- let(:user) { create(:user, :main_bartender) }
+ let(:action_user) { create(:user, :main_bartender) }
it { expect(request.status).to eq 403 }
end
describe 'when as renting-manager' do
- let(:user) { create(:user, :renting_manager) }
+ let(:action_user) { create(:user, :renting_manager) }
it { expect(request.status).to eq 403 }
end
describe 'when as treasurer' do
- let(:user) { create(:user, :treasurer) }
+ let(:action_user) { create(:user, :treasurer) }
it { expect(request.status).to eq 302 }
it { expect(user.name).to eq 'New Name' }
diff --git a/spec/controllers/users_controller/update_with_sofia_account_spec.rb b/spec/controllers/users_controller/update_with_sofia_account_spec.rb
new file mode 100644
index 000000000..de53882bd
--- /dev/null
+++ b/spec/controllers/users_controller/update_with_sofia_account_spec.rb
@@ -0,0 +1,55 @@
+require 'rails_helper'
+
+describe UsersController, type: :controller do
+ describe 'PATCH update_with_sofia_account' do
+ let(:user) { create(:user, :sofia_account, name: 'Old name') }
+ let(:sofia_account) { create(:sofia_account, user:, username: 'Old username') }
+ let(:sofia_account_attributes) { { username: 'AAAA' } }
+ let(:request) do
+ patch :update_with_sofia_account,
+ params: { id: user.id, user: user.attributes.merge({ sofia_account_attributes: sofia_account.attributes }) }
+ end
+
+ before do
+ user
+ sofia_account
+ sign_in action_user
+ user.sofia_account.username = 'New username'
+ user.name = 'New name'
+ request
+ user.reload
+ end
+
+ describe 'when as user themselves' do
+ let(:action_user) { user }
+
+ it { expect(request.status).to eq 302 }
+ it { expect(user.sofia_account.username).to eq 'New username' }
+ it { expect(user.name).to eq 'Old name' }
+ end
+
+ describe 'when as another user' do
+ let(:action_user) { create(:user) }
+
+ it { expect(request.status).to eq 403 }
+ end
+
+ describe 'when as main-bartender' do
+ let(:action_user) { create(:user, :main_bartender) }
+
+ it { expect(request.status).to eq 403 }
+ end
+
+ describe 'when as renting-manager' do
+ let(:action_user) { create(:user, :renting_manager) }
+
+ it { expect(request.status).to eq 403 }
+ end
+
+ describe 'when as treasurer' do
+ let(:action_user) { create(:user, :treasurer) }
+
+ it { expect(request.status).to eq 403 }
+ end
+ end
+end
diff --git a/spec/factories/sofia_account.rb b/spec/factories/sofia_account.rb
new file mode 100644
index 000000000..c8e7895ef
--- /dev/null
+++ b/spec/factories/sofia_account.rb
@@ -0,0 +1,11 @@
+FactoryBot.define do
+ factory :sofia_account do
+ username { Faker::Internet.username }
+ password { Faker::Internet.password(min_length: 12, max_length: 30) }
+ user { create(:user, :sofia_account) }
+
+ trait :otp_enabled do
+ otp_enabled { true }
+ end
+ end
+end
diff --git a/spec/factories/user.rb b/spec/factories/user.rb
index 2dedf1802..f472e9aa0 100644
--- a/spec/factories/user.rb
+++ b/spec/factories/user.rb
@@ -26,6 +26,10 @@
provider { 'amber_oauth2' }
end
+ trait(:sofia_account) do
+ provider { 'sofia_account' }
+ end
+
trait(:manual) do
provider { nil }
end
diff --git a/spec/mailers/previews/user_mailer_preview.rb b/spec/mailers/previews/user_mailer_preview.rb
new file mode 100644
index 000000000..a7a9728a8
--- /dev/null
+++ b/spec/mailers/previews/user_mailer_preview.rb
@@ -0,0 +1,18 @@
+# Preview all emails at http://localhost:5000/rails/mailers/user_mailer
+class UserMailerPreview < ActionMailer::Preview
+ def account_creation_mail
+ user = User.new(id: 999_999, provider: 'sofia_account', activation_token: 'mockup_activation_token')
+ UserMailer.account_creation_email(user)
+ end
+
+ def forgot_password_mail
+ user = User.new(id: 999_999, provider: 'sofia_account', activation_token: 'mockup_activation_token')
+ SofiaAccount.new(id: 888_888, user:, username: 'mockup_username')
+ UserMailer.forgot_password_email(user)
+ end
+
+ def new_activation_link_mail
+ user = User.new(id: 999_999, provider: 'sofia_account', activation_token: 'mockup_activation_token')
+ UserMailer.new_activation_link_email(user)
+ end
+end
diff --git a/spec/models/role_spec.rb b/spec/models/role_spec.rb
index 7e16e5980..e6fb489d7 100644
--- a/spec/models/role_spec.rb
+++ b/spec/models/role_spec.rb
@@ -11,12 +11,6 @@
it { expect(role).not_to be_valid }
end
-
- context 'when without group_uid' do
- subject(:role) { build_stubbed(:role, group_uid: nil) }
-
- it { expect(role).not_to be_valid }
- end
end
describe '#name' do
diff --git a/spec/models/sofia_account_spec.rb b/spec/models/sofia_account_spec.rb
new file mode 100644
index 000000000..1f7a72551
--- /dev/null
+++ b/spec/models/sofia_account_spec.rb
@@ -0,0 +1,99 @@
+require 'rails_helper'
+
+RSpec.describe SofiaAccount, type: :model do
+ subject(:sofia_account) do
+ build_stubbed(:sofia_account, user: build_stubbed(:user, activation_token: SecureRandom.urlsafe_base64), password: 'password1234')
+ end
+
+ describe '#valid?' do
+ it { expect(sofia_account).to be_valid }
+
+ context 'with username' do
+ context 'when without' do
+ subject(:sofia_account) { build_stubbed(:sofia_account, username: nil) }
+
+ it { expect(sofia_account).not_to be_valid }
+ end
+
+ context 'when containing spaces' do
+ subject(:sofia_account) { build_stubbed(:sofia_account, username: 'contains two spaces') }
+
+ it { expect(sofia_account).to be_valid }
+ end
+
+ context 'when empty string' do
+ subject(:sofia_account) { build_stubbed(:sofia_account, username: '') }
+
+ it { expect(sofia_account).not_to be_valid }
+ end
+ end
+
+ context 'with password' do
+ context 'when without' do
+ subject(:sofia_account) do
+ build_stubbed(:sofia_account, password: nil)
+ end
+
+ it { expect(sofia_account).not_to be_valid }
+ end
+
+ context 'when too short' do
+ subject(:sofia_account) do
+ build_stubbed(:sofia_account, password: '<12char')
+ end
+
+ it { expect(sofia_account).not_to be_valid }
+ end
+
+ context 'when empty string' do
+ subject(:sofia_account) { build_stubbed(:sofia_account, password: '') }
+
+ it { expect(sofia_account).not_to be_valid }
+ end
+
+ context 'when long enough' do
+ subject(:sofia_account) { build_stubbed(:sofia_account, password: 'aaaaaaaaaaaa') }
+
+ it { expect(sofia_account).to be_valid }
+ end
+ end
+
+ context 'when having duplicate fields' do
+ let(:sofia_account) { create(:sofia_account, password: 'password1234') }
+
+ context 'with username' do
+ subject(:duplicate_sofia_account) { build(:sofia_account, username: sofia_account.username) }
+
+ it { expect(duplicate_sofia_account).not_to be_valid }
+ end
+
+ context 'with password' do
+ subject(:duplicate_sofia_account) { build_stubbed(:sofia_account, password: sofia_account.password) }
+
+ it { expect(duplicate_sofia_account).to be_valid }
+ end
+
+ context 'with user' do
+ subject(:duplicate_sofia_account) { build_stubbed(:sofia_account, user: sofia_account.user) }
+
+ it { expect(duplicate_sofia_account).not_to be_valid }
+ end
+ end
+ end
+
+ describe 'activate_account_url' do
+ it { expect(described_class.activate_account_url(sofia_account.user.id, sofia_account.user.activation_token)).to eq "http://testhost:1337/sofia_accounts/activate_account?activation_token=#{sofia_account.user.activation_token}&user_id=#{sofia_account.user.id}" }
+ end
+
+ describe 'new_activation_link_url' do
+ it { expect(described_class.new_activation_link_url(sofia_account.user.id)).to eq "http://testhost:1337/sofia_accounts/new_activation_link?user_id=#{sofia_account.user.id}" }
+ end
+
+ describe 'forgot_password_url' do
+ it { expect(described_class.forgot_password_url).to eq 'http://testhost:1337/sofia_accounts/forgot_password' }
+ end
+
+ describe 'reset_password_url' do
+ it { expect(sofia_account.reset_password_url(sofia_account.user.activation_token)).to eq "http://testhost:1337/sofia_accounts/#{sofia_account.id}/reset_password?activation_token=#{sofia_account.user.activation_token}" }
+ end
+end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 647d334f8..6dba2f18d 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -12,6 +12,12 @@
it { expect(user).not_to be_valid }
end
+ context 'when with invalid email' do
+ subject(:user) { build_stubbed(:user, email: 'not_an_valid_email') }
+
+ it { expect(user).not_to be_valid }
+ end
+
context 'when deactivating with credit' do
subject(:user) { create(:user, deactivated: true) }
@@ -41,6 +47,40 @@
end
end
+ describe '.sofia_account' do
+ context 'when sofia_account' do
+ subject(:user) { create(:user, :sofia_account) }
+
+ before { user }
+
+ it { expect(described_class.sofia_account).to include user }
+ end
+
+ context 'when not sofia_account' do
+ subject(:user) { create(:user, provider: 'another_provider') }
+
+ before { user }
+
+ it { expect(described_class.sofia_account).not_to include user }
+ end
+
+ context 'when without email' do
+ subject(:user) { build(:user, :sofia_account, email: nil) }
+
+ before { build(:sofia_account, user:) }
+
+ it { expect(user).not_to be_valid }
+ end
+
+ context 'when with valid email' do
+ subject(:user) { build(:user, :sofia_account, email: 'valid@email.com') }
+
+ before { build(:sofia_account, user:) }
+
+ it { expect(user).to be_valid }
+ end
+ end
+
describe '.manual' do
context 'when manual created' do
subject(:user) { create(:user, provider: nil) }
@@ -75,19 +115,29 @@
end
end
- describe '.active / .inactive' do
+ describe '.active / .deactivated / .not_activated' do
context 'when active' do
subject(:user) { create(:user) }
it { expect(described_class.active).to include user }
- it { expect(described_class.inactive).not_to include user }
+ it { expect(described_class.not_activated).not_to include user }
+ it { expect(described_class.deactivated).not_to include user }
end
context 'when deactivated' do
subject(:user) { create(:user, deactivated: true) }
it { expect(described_class.active).not_to include user }
- it { expect(described_class.inactive).to include user }
+ it { expect(described_class.not_activated).not_to include user }
+ it { expect(described_class.deactivated).to include user }
+ end
+
+ context 'when not activated' do
+ subject(:user) { create(:user, :sofia_account) }
+
+ it { expect(described_class.active).not_to include user }
+ it { expect(described_class.not_activated).to include user }
+ it { expect(described_class.deactivated).not_to include user }
end
end
@@ -224,7 +274,7 @@
describe '#update_role' do
context 'when getting new roles' do
- subject(:user) { create(:user) }
+ subject(:user) { create(:user, :from_amber) }
let(:role) { create(:role) }
@@ -388,15 +438,32 @@
end
end
+ describe 'destroy' do
+ context 'with sofia_account' do
+ subject(:user) { create(:user, :sofia_account) }
+
+ before do
+ create(:sofia_account, user:)
+ user.destroy
+ end
+
+ it { expect(SofiaAccount.count).to eq 0 }
+ end
+ end
+
describe '#archive!' do
- context 'when archiving a user' do
- subject(:user) { create(:user) }
+ context 'with sofia_account' do
+ subject(:user) { create(:user, :sofia_account) }
let(:nil_attributes) do
%w[avatar_thumb_url email birthday]
end
- before { user.archive! && user.reload }
+ before do
+ create(:sofia_account, user:)
+ user.archive!
+ user.reload
+ end
it { expect(user.archive!).to be true }
it { expect(user.name).to eq "Gearchiveerde gebruiker #{user.id}" }
@@ -405,6 +472,8 @@
it { expect { user.archive! }.not_to(change(user, :id)) }
it { expect { user.archive! }.not_to(change(user, :uid)) }
it { expect { user.archive! }.not_to(change(user, :provider)) }
+ it { expect { user.archive! }.not_to(change(user, :sofia_account)) }
+ it { expect(SofiaAccount.count).to eq 1 }
it do
nil_attributes.each do |attribute|
@@ -413,4 +482,105 @@
end
end
end
+
+ describe '#before_save' do
+ context 'with sofia_account' do
+ subject(:user) { create(:user, :sofia_account) }
+
+ it { expect(user.activation_token).not_to be_nil }
+
+ it do
+ expect(user.activation_token_valid_till).not_to be_nil
+ expect(user.activation_token_valid_till.day).to eq 5.days.from_now.day
+ end
+ end
+
+ context 'without sofia_account' do
+ subject(:user) { create(:user) }
+
+ it { expect(user.activation_token).to be_nil }
+ it { expect(user.activation_token_valid_till).to be_nil }
+ end
+ end
+
+ describe '#after_save' do
+ context 'when deactivated upon creation' do
+ subject(:user) { build(:user, deactivated: true) }
+
+ it do
+ expect(user).to receive(:archive!)
+ user.save
+ end
+ end
+
+ context 'when deactivated after update' do
+ subject(:user) { create(:user, deactivated: false) }
+
+ before { user.deactivated = true }
+
+ it do
+ expect(user).to receive(:archive!)
+ user.save
+ end
+ end
+
+ context 'when deactivated and not changed' do
+ subject(:user) { create(:user, deactivated: true) }
+
+ before { user.email = 'valid@email.com' }
+
+ it do
+ expect(user).not_to receive(:archive!)
+ user.save
+ end
+ end
+
+ context 'when not deactivated upon creation' do
+ subject(:user) { build(:user, deactivated: false) }
+
+ it do
+ expect(user).not_to receive(:archive!)
+ user.save
+ end
+ end
+
+ context 'when not deactivated after update' do
+ subject(:user) { create(:user, deactivated: true) }
+
+ before { user.deactivated = false }
+
+ it do
+ expect(user).not_to receive(:archive!)
+ user.save
+ end
+ end
+ end
+
+ describe '#after_create' do
+ context 'with sofia_account' do
+ subject(:user) { build(:user, :sofia_account) }
+
+ after do
+ clear_enqueued_jobs
+ end
+
+ it do
+ user.save
+ expect(UserMailer).to send_email(:account_creation_email, :deliver_later, user)
+ end
+ end
+
+ context 'without sofia_account' do
+ subject(:user) { build(:user) }
+
+ after do
+ clear_enqueued_jobs
+ end
+
+ it do
+ expect(UserMailer).not_to receive(:account_creation_email)
+ user.save
+ end
+ end
+ end
end
diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb
index 8866ea435..e328720c1 100644
--- a/spec/rails_helper.rb
+++ b/spec/rails_helper.rb
@@ -31,6 +31,7 @@
RSpec.configure do |config|
config.include ActiveJob::TestHelper
config.include Devise::Test::ControllerHelpers, type: :controller
+ config.include DeviseHelper, type: :request
# Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
config.fixture_path = Rails.root.join('spec', 'fixtures')
diff --git a/spec/support/devise_helper.rb b/spec/support/devise_helper.rb
new file mode 100644
index 000000000..c6ab4dde0
--- /dev/null
+++ b/spec/support/devise_helper.rb
@@ -0,0 +1,13 @@
+module DeviseHelper
+ def signed_in?(user)
+ user == signed_in_user
+ end
+
+ def signed_in_user
+ user_session_info = response.request.env['rack.session']['warden.user.user.key']
+ return unless user_session_info
+
+ user_id = user_session_info[0][0]
+ User.find(user_id)
+ end
+end
diff --git a/spec/support/mailer_matcher.rb b/spec/support/mailer_matcher.rb
new file mode 100644
index 000000000..de3acf7ee
--- /dev/null
+++ b/spec/support/mailer_matcher.rb
@@ -0,0 +1,9 @@
+require 'rspec/expectations'
+
+RSpec::Matchers.define :send_email do |mailer_action, mailer_when, mailer_args|
+ match do |mailer_class|
+ message_delivery = instance_double(ActionMailer::MessageDelivery)
+ expect(mailer_class).to receive(mailer_action).with(mailer_args).and_return(message_delivery)
+ allow(message_delivery).to receive(mailer_when)
+ end
+end