diff --git a/Rakefile b/Rakefile
index 488c551f..92772ad0 100644
--- a/Rakefile
+++ b/Rakefile
@@ -4,5 +4,12 @@
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
require_relative 'config/application'
+require 'rake/testtask'
+
+Rake::TestTask.new do |t|
+ t.libs << "test"
+ t.test_files = FileList['test/test*.rb']
+ t.verbose = true
+end
Rails.application.load_tasks
diff --git a/app/controllers/rooms_controller.rb b/app/controllers/rooms_controller.rb
index 0220dae6..857819ec 100644
--- a/app/controllers/rooms_controller.rb
+++ b/app/controllers/rooms_controller.rb
@@ -18,6 +18,7 @@
class RoomsController < ApplicationController
before_action :validate_accepted_terms, unless: -> { !Rails.configuration.terms }
+ before_action :validate_verified_email, unless: -> { !Rails.configuration.enable_email_verification }
before_action :find_room, except: :create
before_action :verify_room_ownership, except: [:create, :show, :join, :logout]
@@ -184,4 +185,10 @@ class RoomsController < ApplicationController
redirect_to terms_path unless current_user.accepted_terms
end
end
+
+ def validate_verified_email
+ if current_user
+ redirect_to resend_path unless current_user.email_verified
+ end
+ end
end
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 968f6853..f3f29e3f 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -28,7 +28,10 @@ class UsersController < ApplicationController
@user = User.new(user_params)
@user.provider = "greenlight"
- if @user.save
+ if Rails.configuration.enable_email_verification && @user.save
+ UserMailer.verify_email(@user, verification_link(@user)).deliver
+ login(@user)
+ elsif @user.save
login(@user)
else
# Handle error on user creation.
@@ -81,6 +84,9 @@ class UsersController < ApplicationController
errors.each { |k, v| @user.errors.add(k, v) }
render :edit, params: { settings: params[:settings] }
end
+ elsif user_params[:email] != @user.email && @user.update_attributes(user_params)
+ @user.update_attributes(email_verified: false)
+ redirect_to edit_user_path(@user), notice: I18n.t("info_update_success")
elsif @user.update_attributes(user_params)
redirect_to edit_user_path(@user), notice: I18n.t("info_update_success")
else
@@ -97,18 +103,50 @@ class UsersController < ApplicationController
redirect_to root_path
end
- # GET /terms
+ # GET | POST /terms
def terms
redirect_to '/404' unless Rails.configuration.terms
if params[:accept] == "true"
current_user.update_attributes(accepted_terms: true)
- redirect_to current_user.main_room if current_user
+ login(current_user)
+ end
+ end
+
+ # GET | POST /u/verify/confirm
+ def confirm
+ if !current_user || current_user.uid != params[:user_uid]
+ redirect_to '/404'
+ elsif current_user.email_verified
+ login(current_user)
+ elsif params[:email_verified] == "true"
+ current_user.update_attributes(email_verified: true)
+ login(current_user)
+ else
+ render 'verify'
+ end
+ end
+
+ # GET /u/verify/resend
+ def resend
+ if !current_user
+ redirect_to '/404'
+ elsif current_user.email_verified
+ login(current_user)
+ elsif params[:email_verified] == "false"
+ UserMailer.verify_email(current_user, verification_link(current_user)).deliver
+ render 'verify'
+ else
+ render 'verify'
end
end
private
+ def verification_link(user)
+ request.base_url + confirm_path(user.uid)
+ end
+
def find_user
@user = User.find_by!(uid: params[:user_uid])
end
diff --git a/app/helpers/sessions_helper.rb b/app/helpers/sessions_helper.rb
index fa64adfd..2b66807e 100644
--- a/app/helpers/sessions_helper.rb
+++ b/app/helpers/sessions_helper.rb
@@ -21,14 +21,23 @@ module SessionsHelper
def login(user)
session[:user_id] = user.id
- # If there are not terms, or the user has accepted them, go to their room.
+ # If there are not terms, or the user has accepted them, check for email verification
if !Rails.configuration.terms || user.accepted_terms
- redirect_to user.main_room
+ check_email_verified(user)
else
redirect_to terms_path
end
end
+ # If email verification is disabled, or the user has verified, go to their room
+ def check_email_verified(user)
+ if !Rails.configuration.enable_email_verification || user.email_verified
+ redirect_to user.main_room
+ else
+ redirect_to resend_path
+ end
+ end
+
# Logs current user out of GreenLight.
def logout
session.delete(:user_id) if current_user
diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb
new file mode 100644
index 00000000..ecab8127
--- /dev/null
+++ b/app/mailers/user_mailer.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/.
+#
+# Copyright (c) 2018 BigBlueButton Inc. and by respective authors (see below).
+#
+# This program is free software; you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free Software
+# Foundation; either version 3.0 of the License, or (at your option) any later
+# version.
+#
+# BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with BigBlueButton; if not, see .
+
+class UserMailer < ApplicationMailer
+ default from: 'notifications@example.com'
+
+ def verify_email(user, url)
+ @user = user
+ @url = url
+ mail(to: @user.email, subject: 'Welcome to BigBlueButton!')
+ end
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index 84f824e8..68dcb033 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -51,6 +51,7 @@ class User < ApplicationRecord
u.username = auth_username(auth) unless u.username
u.email = auth_email(auth)
u.image = auth_image(auth)
+ u.email_verified = true
u.save!
end
end
diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb
index 179b214d..88b46fc9 100644
--- a/app/views/layouts/mailer.html.erb
+++ b/app/views/layouts/mailer.html.erb
@@ -1,6 +1,8 @@
<%
# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/.
+#
# Copyright (c) 2018 BigBlueButton Inc. and by respective authors (see below).
+#
# This program is free software; you can redistribute it and/or modify it under the
# terms of the GNU Lesser General Public License as published by the Free Software
# Foundation; either version 3.0 of the License, or (at your option) any later
@@ -9,6 +11,7 @@
# BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+#
# You should have received a copy of the GNU Lesser General Public License along
# with BigBlueButton; if not, see .
%>
diff --git a/app/views/layouts/mailer.text.erb b/app/views/layouts/mailer.text.erb
index c6f81bba..14f380cb 100644
--- a/app/views/layouts/mailer.text.erb
+++ b/app/views/layouts/mailer.text.erb
@@ -1,6 +1,8 @@
<%
# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/.
+#
# Copyright (c) 2018 BigBlueButton Inc. and by respective authors (see below).
+#
# This program is free software; you can redistribute it and/or modify it under the
# terms of the GNU Lesser General Public License as published by the Free Software
# Foundation; either version 3.0 of the License, or (at your option) any later
@@ -9,6 +11,7 @@
# BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+#
# You should have received a copy of the GNU Lesser General Public License along
# with BigBlueButton; if not, see .
%>
diff --git a/app/views/shared/components/_confirm_button.html.erb b/app/views/shared/components/_confirm_button.html.erb
new file mode 100644
index 00000000..8fdba35b
--- /dev/null
+++ b/app/views/shared/components/_confirm_button.html.erb
@@ -0,0 +1,18 @@
+<%
+# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/.
+# Copyright (c) 2018 BigBlueButton Inc. and by respective authors (see below).
+# This program is free software; you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free Software
+# Foundation; either version 3.0 of the License, or (at your option) any later
+# version.
+#
+# BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+# You should have received a copy of the GNU Lesser General Public License along
+# with BigBlueButton; if not, see .
+%>
+
+
+ <%= button_to t("verify.accept"), confirm_path, params: { user_uid: params[:user_uid], email_verified: true }, class: "btn btn-primary btn-space" %>
+
diff --git a/app/views/shared/components/_resend_button.html.erb b/app/views/shared/components/_resend_button.html.erb
new file mode 100644
index 00000000..914c85e6
--- /dev/null
+++ b/app/views/shared/components/_resend_button.html.erb
@@ -0,0 +1,18 @@
+<%
+# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/.
+# Copyright (c) 2018 BigBlueButton Inc. and by respective authors (see below).
+# This program is free software; you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free Software
+# Foundation; either version 3.0 of the License, or (at your option) any later
+# version.
+#
+# BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+# You should have received a copy of the GNU Lesser General Public License along
+# with BigBlueButton; if not, see .
+%>
+
+
+ <%= button_to t("verify.resend"), resend_path, params: { email_verified: false }, class: "btn btn-primary btn-space" %>
+
diff --git a/app/views/shared/settings/_account.html.erb b/app/views/shared/settings/_account.html.erb
index d6cf39f9..4a8eb00b 100644
--- a/app/views/shared/settings/_account.html.erb
+++ b/app/views/shared/settings/_account.html.erb
@@ -27,7 +27,7 @@
<%= f.label t("email"), class: "form-label" %>
- <%= f.text_field :email, class: "form-control #{form_is_invalid?(@user, :email)}", placeholder: t("email") %>
+ <%= f.text_field :email, class: "form-control #{form_is_invalid?(@user, :email)}", placeholder: t("email"), readonly: !current_user.greenlight_account? %>
diff --git a/app/views/user_mailer/verify_email.html.erb b/app/views/user_mailer/verify_email.html.erb
new file mode 100644
index 00000000..7cda6e2e
--- /dev/null
+++ b/app/views/user_mailer/verify_email.html.erb
@@ -0,0 +1,35 @@
+<%
+# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/.
+#
+# Copyright (c) 2018 BigBlueButton Inc. and by respective authors (see below).
+#
+# This program is free software; you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free Software
+# Foundation; either version 3.0 of the License, or (at your option) any later
+# version.
+#
+# BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with BigBlueButton; if not, see .
+%>
+
+
+
+
+
+
+
+ Welcome to Greenlight!, <%= @user.name %>
+
+ You have successfully signed up for Greenlight,
+ your username is: <%= @user.email %>.
+
+
+ To verify your account, just follow this link: <%= link_to 'verify your email', @url %>.
+
+ Thanks for joining and have a great day!
+
+
diff --git a/app/views/user_mailer/verify_email.text.erb b/app/views/user_mailer/verify_email.text.erb
new file mode 100644
index 00000000..9448e553
--- /dev/null
+++ b/app/views/user_mailer/verify_email.text.erb
@@ -0,0 +1,28 @@
+<%
+# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/.
+#
+# Copyright (c) 2018 BigBlueButton Inc. and by respective authors (see below).
+#
+# This program is free software; you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free Software
+# Foundation; either version 3.0 of the License, or (at your option) any later
+# version.
+#
+# BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with BigBlueButton; if not, see .
+%>
+
+
+Welcome to Greenlight, <%= @user.name %>
+===============================================
+
+You have successfully signed up for Greenlight,
+your username is: <%= @user.email %>.
+
+To verify your account, just follow this link: <%= link_to 'verify your email', @url %>.
+
+Thanks for joining and have a great day!
diff --git a/app/views/users/verify.html.erb b/app/views/users/verify.html.erb
new file mode 100644
index 00000000..49815d14
--- /dev/null
+++ b/app/views/users/verify.html.erb
@@ -0,0 +1,33 @@
+<%
+# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/.
+# Copyright (c) 2018 BigBlueButton Inc. and by respective authors (see below).
+# This program is free software; you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free Software
+# Foundation; either version 3.0 of the License, or (at your option) any later
+# version.
+#
+# BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+# You should have received a copy of the GNU Lesser General Public License along
+# with BigBlueButton; if not, see .
+%>
+
+
+
+
+
+
+
Your account has not been verified yet.
+ <% if Rails.configuration.enable_email_verification && params[:user_uid] == current_user.uid %>
+ <%= render "/shared/components/confirm_button" %>
+ <% else %>
+ <%= render "/shared/components/resend_button" %>
+ <% end %>
+
+
+
+
+
diff --git a/config/application.rb b/config/application.rb
index 8bade271..6f8c5343 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -68,6 +68,9 @@ module Greenlight
config.bigbluebutton_endpoint += "api/" unless config.bigbluebutton_endpoint.ends_with?('api/')
end
+ # Determine if GreenLight should enable email verification
+ config.enable_email_verification = (ENV['GREENLIGHT_MAIL_NOTIFICATIONS'] == "true")
+
# Determine if GreenLight should allow non-omniauth signup/login.
config.allow_user_signup = (ENV['ALLOW_GREENLIGHT_ACCOUNTS'] == "true")
diff --git a/config/environments/development.rb b/config/environments/development.rb
index 3027e432..672afa10 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -28,6 +28,19 @@ Rails.application.configure do
config.cache_store = :null_store
end
+ # Tell Action Mailer to use smtp server
+ config.action_mailer.delivery_method = :smtp
+
+ ActionMailer::Base.smtp_settings = {
+ address: ENV['SMTP_SERVER'],
+ port: ENV["SMTP_PORT"],
+ domain: ENV['SMTP_DOMAIN'],
+ user_name: ENV['SMTP_USERNAME'],
+ password: ENV['SMTP_PASSWORD'],
+ authentication: ENV['SMTP_AUTH'],
+ enable_starttls_auto: ENV['SMTP_STARTTLS_AUTO'],
+ }
+
# Don't care if the mailer can't send.
config.action_mailer.raise_delivery_errors = false
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 82fe17c7..67f2989c 100755
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -164,3 +164,7 @@ en:
This deployment is using a pre-configured testing server, you should replace this with your own.
For details, see the %{href}.
update: Update
+ verify:
+ title: Verify your email
+ resend: Resend verification email
+ accept: Verify
diff --git a/config/routes.rb b/config/routes.rb
index 66b7d111..c8f40630 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -31,6 +31,12 @@ Rails.application.routes.draw do
# User resources.
scope '/u' do
+ # Verification Routes
+ scope '/verify' do
+ match '/resend', to: 'users#resend', via: [:get, :post], as: :resend
+ match '/confirm/:user_uid', to: 'users#confirm', via: [:get, :post], as: :confirm
+ end
+
# Handles login of greenlight provider accounts.
post '/login', to: 'sessions#create', as: :create_session
diff --git a/db/migrate/20180920193451_add_email_verified_to_user.rb b/db/migrate/20180920193451_add_email_verified_to_user.rb
new file mode 100644
index 00000000..a6611920
--- /dev/null
+++ b/db/migrate/20180920193451_add_email_verified_to_user.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/.
+#
+# Copyright (c) 2018 BigBlueButton Inc. and by respective authors (see below).
+#
+# This program is free software; you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free Software
+# Foundation; either version 3.0 of the License, or (at your option) any later
+# version.
+#
+# BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with BigBlueButton; if not, see .
+
+class AddEmailVerifiedToUser < ActiveRecord::Migration[5.0]
+ def change
+ add_column :users, :email_verified, :boolean, default: false
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 53f29e75..c90470ee 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20180504131705) do
+ActiveRecord::Schema.define(version: 20180920193451) do
create_table "rooms", force: :cascade do |t|
t.integer "user_id"
@@ -42,6 +42,7 @@ ActiveRecord::Schema.define(version: 20180504131705) do
t.boolean "accepted_terms", default: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.boolean "email_verified", default: false
t.index ["password_digest"], name: "index_users_on_password_digest", unique: true
t.index ["room_id"], name: "index_users_on_room_id"
end
diff --git a/sample.env b/sample.env
index 78959d35..7b47ab2c 100644
--- a/sample.env
+++ b/sample.env
@@ -61,6 +61,25 @@ LDAP_PASSWORD=
#
ALLOW_GREENLIGHT_ACCOUNTS=true
+# Set this to true if you want GreenLight to send verification emails upon
+# the creation of a new account
+#
+# SMTP variables can be taken from the list in the following table:
+#
+# (SMTP_SERVER= SMTP SETTINGS)
+# (SMTP_DOMAIN= URL)
+#
+# https://serversmtp.com/smtp-server-address/
+#
+GREENLIGHT_MAIL_NOTIFICATIONS=true
+SMTP_SERVER=smtp.gmail.com
+SMTP_PORT=587
+SMTP_DOMAIN=gmail.com
+SMTP_USERNAME=youremail@gmail.com
+SMTP_PASSWORD=yourpassword
+SMTP_AUTH=plain
+SMTP_STARTTLS_AUTO=true
+
# Prefix for the applications root URL.
# Useful for deploying the application to a subdirectory, which is highly recommended
# if deploying on a BigBlueButton server. Keep in mind that if you change this, you'll
diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb
index fd1c18b2..ad06b32e 100644
--- a/spec/controllers/users_controller_spec.rb
+++ b/spec/controllers/users_controller_spec.rb
@@ -27,6 +27,7 @@ def random_valid_user_params
password: pass,
password_confirmation: pass,
accepted_terms: true,
+ email_verified: true,
},
}
end
@@ -40,6 +41,7 @@ describe UsersController, type: :controller do
password: "pass",
password_confirmation: "invalid",
accepted_terms: false,
+ email_verified: false,
},
}
end
@@ -56,6 +58,7 @@ describe UsersController, type: :controller do
describe "POST #create" do
context "allow greenlight accounts" do
before { allow(Rails.configuration).to receive(:allow_user_signup).and_return(true) }
+ before { allow(Rails.configuration).to receive(:enable_email_verification).and_return(false) }
it "redirects to user room on successful create" do
params = random_valid_user_params
@@ -65,6 +68,7 @@ describe UsersController, type: :controller do
expect(u).to_not be_nil
expect(u.name).to eql(params[:user][:name])
+
expect(response).to redirect_to(room_path(u.main_room))
end
@@ -125,4 +129,57 @@ describe UsersController, type: :controller do
expect(response).to render_template(:edit)
end
end
+
+ describe "GET | POST #resend" do
+ before { allow(Rails.configuration).to receive(:allow_user_signup).and_return(true) }
+ before { allow(Rails.configuration).to receive(:enable_email_verification).and_return(true) }
+
+ it "redirects to main room if verified" do
+ params = random_valid_user_params
+ post :create, params: params
+
+ u = User.find_by(name: params[:user][:name], email: params[:user][:email])
+ u.email_verified = false
+
+ get :resend
+ expect(response).to render_template(:verify)
+ end
+
+ it "resend email upon click if unverified" do
+ params = random_valid_user_params
+ post :create, params: params
+
+ u = User.find_by(name: params[:user][:name], email: params[:user][:email])
+ u.email_verified = false
+
+ expect { post :resend, params: { email_verified: false } }.to change { ActionMailer::Base.deliveries.count }.by(1)
+ expect(response).to render_template(:verify)
+ end
+ end
+
+ describe "GET | POST #confirm" do
+ before { allow(Rails.configuration).to receive(:allow_user_signup).and_return(true) }
+ before { allow(Rails.configuration).to receive(:enable_email_verification).and_return(true) }
+
+ it "redirects to main room if already verified" do
+ params = random_valid_user_params
+ post :create, params: params
+
+ u = User.find_by(name: params[:user][:name], email: params[:user][:email])
+
+ post :confirm, params: { user_uid: u.uid, email_verified: true }
+ expect(response).to redirect_to(room_path(u.main_room))
+ end
+
+ it "renders confirmation pane if unverified" do
+ params = random_valid_user_params
+ post :create, params: params
+
+ u = User.find_by(name: params[:user][:name], email: params[:user][:email])
+ u.email_verified = false
+
+ get :confirm, params: { user_uid: u.uid }
+ expect(response).to render_template(:verify)
+ end
+ end
end
diff --git a/spec/factories.rb b/spec/factories.rb
index 7d4cd00c..e62cceb0 100644
--- a/spec/factories.rb
+++ b/spec/factories.rb
@@ -28,6 +28,7 @@ FactoryBot.define do
password { password }
password_confirmation { password }
accepted_terms { true }
+ email_verified { true }
end
factory :room do
diff --git a/test/mailers/previews/user_mailer_preview.rb b/test/mailers/previews/user_mailer_preview.rb
new file mode 100644
index 00000000..18cef94f
--- /dev/null
+++ b/test/mailers/previews/user_mailer_preview.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+class UserMailerPreview < ActionMailer::Preview
+end
diff --git a/test/mailers/user_mailer_test.rb b/test/mailers/user_mailer_test.rb
new file mode 100644
index 00000000..03000865
--- /dev/null
+++ b/test/mailers/user_mailer_test.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+require 'test_helper'
+
+class UserMailerTest < ActionMailer::TestCase
+end
diff --git a/test/test_helper.rb b/test/test_helper.rb
new file mode 100644
index 00000000..e69de29b