diff --git a/Gemfile b/Gemfile
index e2c43009..c5e2aa88 100644
--- a/Gemfile
+++ b/Gemfile
@@ -24,6 +24,9 @@ gem "turbo-rails"
# Authorization
gem "action_policy", "~> 0.7.0"
+# Authentication
+gem "bcrypt", "~> 3.1.20"
+
# Other
gem "bootsnap", require: false
gem "puma", ">= 5.0"
@@ -41,7 +44,8 @@ group :development, :test do
gem "dotenv"
gem "erb_lint", require: false
gem "factory_bot_rails"
- gem "letter_opener"
+ gem "faker"
+ gem "letter_opener", "~> 1.10"
gem "pry-byebug"
gem "rspec-rails"
gem "rubocop-capybara", require: false
@@ -55,7 +59,6 @@ end
group :development do
gem "annotate"
- gem "faker"
gem "rack-mini-profiler"
gem "web-console"
end
@@ -64,6 +67,7 @@ group :test do
gem "capybara"
gem "cuprite"
gem "fuubar"
+ gem "rails-controller-testing"
gem "rspec-instafail", require: false
gem "rspec-retry"
end
diff --git a/Gemfile.lock b/Gemfile.lock
index d699a233..b845e5a0 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -91,6 +91,7 @@ GEM
ice_nine (~> 0.11.0)
thread_safe (~> 0.3, >= 0.3.1)
base64 (0.2.0)
+ bcrypt (3.1.20)
better_errors (2.10.1)
erubi (>= 1.0.0)
rack (>= 0.9.0)
@@ -315,6 +316,10 @@ GEM
activesupport (= 7.1.3.4)
bundler (>= 1.15.0)
railties (= 7.1.3.4)
+ rails-controller-testing (1.0.5)
+ actionpack (>= 5.0.1.rc1)
+ actionview (>= 5.0.1.rc1)
+ activesupport (>= 5.0.1.rc1)
rails-dom-testing (2.2.0)
activesupport (>= 5.0.0)
minitest
@@ -485,6 +490,7 @@ GEM
PLATFORMS
arm64-darwin-21
+ arm64-darwin-22
arm64-darwin-23
x86_64-linux
@@ -492,6 +498,7 @@ DEPENDENCIES
action_policy (~> 0.7.0)
activerecord-enhancedsqlite3-adapter (~> 0.8.0)
annotate
+ bcrypt (~> 3.1.20)
better_errors
binding_of_caller
bootsnap
@@ -507,13 +514,14 @@ DEPENDENCIES
faker
fuubar
importmap-rails
- letter_opener
+ letter_opener (~> 1.10)
mission_control-jobs
propshaft
pry-byebug
puma (>= 5.0)
rack-mini-profiler
rails (~> 7.1.3, >= 7.1.3.4)
+ rails-controller-testing
rspec-instafail
rspec-rails
rspec-retry
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 09705d12..658bada1 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -1,2 +1,5 @@
class ApplicationController < ActionController::Base
+ include Authentication
+
+ before_action :authenticate_user!
end
diff --git a/app/controllers/concerns/authentication.rb b/app/controllers/concerns/authentication.rb
new file mode 100644
index 00000000..1bb4e35a
--- /dev/null
+++ b/app/controllers/concerns/authentication.rb
@@ -0,0 +1,34 @@
+module Authentication
+ extend ActiveSupport::Concern
+
+ included do
+ helper_method :current_user, :user_signed_in?
+
+ def authenticate_user!
+ redirect_to root_path, alert: t("controllers.concerns.authentication.unauthorized") unless user_signed_in?
+ end
+
+ def current_user
+ Current.user ||= authenticate_user_from_session
+ end
+
+ def authenticate_user_from_session
+ User.find_by(id: session[:user_id])
+ end
+
+ def user_signed_in?
+ current_user.present?
+ end
+
+ def login(user)
+ Current.user = user
+ reset_session
+ session[:user_id] = user.id
+ end
+
+ def logout
+ Current.user = nil
+ reset_session
+ end
+ end
+end
diff --git a/app/controllers/main_controller.rb b/app/controllers/main_controller.rb
new file mode 100644
index 00000000..f6b17f11
--- /dev/null
+++ b/app/controllers/main_controller.rb
@@ -0,0 +1,5 @@
+class MainController < ApplicationController
+ skip_before_action :authenticate_user!
+ def index
+ end
+end
diff --git a/app/controllers/password_resets_controller.rb b/app/controllers/password_resets_controller.rb
new file mode 100644
index 00000000..b79929ac
--- /dev/null
+++ b/app/controllers/password_resets_controller.rb
@@ -0,0 +1,43 @@
+class PasswordResetsController < ApplicationController
+ skip_before_action :authenticate_user!, only: [:new, :create]
+ before_action :set_user_by_token, only: [:edit, :update]
+
+ def new
+ end
+
+ def edit
+ end
+
+ def create
+ @user = User.find_by(email: params[:email])
+
+ if @user.present?
+ PasswordMailer.with(
+ user: @user,
+ token: @user.generate_token_for(:password_reset)
+ ).password_reset.deliver_now
+ end
+
+ redirect_to root_path, notice: t("controllers.password_resets.create.notice")
+ end
+
+ def update
+ if @user.update(password_params)
+ redirect_to new_session_path, notice: t("controllers.password_resets.update.notice")
+ else
+ render :edit, status: :unprocessable_entity
+ end
+ end
+
+ private
+
+ def set_user_by_token
+ @user = User.find_by_token_for(:password_reset, params[:token])
+
+ redirect_to new_password_reset_path alert: t("controllers.password_resets.errors.invalid_token") if @user.blank?
+ end
+
+ def password_params
+ params.require(:user).permit(:password, :password_confirmation)
+ end
+end
diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb
new file mode 100644
index 00000000..82969ec9
--- /dev/null
+++ b/app/controllers/passwords_controller.rb
@@ -0,0 +1,22 @@
+class PasswordsController < ApplicationController
+ def edit
+ end
+
+ def update
+ if current_user.update(password_params)
+ redirect_to edit_password_path, notice: t("controllers.passwords.update.notice")
+ else
+ render :edit, status: :unprocessable_entity
+ end
+ end
+
+ private
+
+ def password_params
+ params.require(:user).permit(
+ :password,
+ :password_confirmation,
+ :password_challenge
+ ).with_defaults(password_challenge: "")
+ end
+end
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
new file mode 100644
index 00000000..199ba626
--- /dev/null
+++ b/app/controllers/registrations_controller.rb
@@ -0,0 +1,24 @@
+class RegistrationsController < ApplicationController
+ skip_before_action :authenticate_user!
+
+ def new
+ @user = User.new
+ end
+
+ def create
+ @user = User.new(registration_params)
+
+ if @user.save
+ login @user
+ redirect_to root_path
+ else
+ render :new, status: :unprocessable_entity
+ end
+ end
+
+ private
+
+ def registration_params
+ params.require(:user).permit(:email, :password, :password_confirmation)
+ end
+end
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
new file mode 100644
index 00000000..ad5454d5
--- /dev/null
+++ b/app/controllers/sessions_controller.rb
@@ -0,0 +1,29 @@
+class SessionsController < ApplicationController
+ skip_before_action :authenticate_user!
+
+ def new
+ end
+
+ def create
+ @user = User.authenticate_by(email: session_params[:email], password: session_params[:password])
+
+ if @user
+ login @user
+ redirect_to root_path, notice: t("controllers.sessions.create.notice")
+ else
+ flash[:alert] = t("controllers.sessions.create.alert")
+ render :new, status: :unprocessable_entity
+ end
+ end
+
+ def destroy
+ logout
+ redirect_to root_path, notice: t("controllers.sessions.destroy.notice")
+ end
+
+ private
+
+ def session_params
+ params.require(:user).permit(:email, :password)
+ end
+end
diff --git a/app/mailers/password_mailer.rb b/app/mailers/password_mailer.rb
new file mode 100644
index 00000000..bfb7aa8d
--- /dev/null
+++ b/app/mailers/password_mailer.rb
@@ -0,0 +1,5 @@
+class PasswordMailer < ApplicationMailer
+ def password_reset
+ mail to: params[:user].email
+ end
+end
diff --git a/app/models/current.rb b/app/models/current.rb
new file mode 100644
index 00000000..73a9744b
--- /dev/null
+++ b/app/models/current.rb
@@ -0,0 +1,3 @@
+class Current < ActiveSupport::CurrentAttributes
+ attribute :user
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index 6f9f0d0b..d37580ee 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -15,6 +15,8 @@
# index_users_on_email (email) UNIQUE
#
class User < ApplicationRecord
+ PASSWORD_RESET_EXPIRATION = 15.minutes
+
normalizes :email, with: ->(email) { email.strip.downcase }
has_one :profile, as: :profileable, dependent: :destroy
@@ -22,4 +24,13 @@ class User < ApplicationRecord
has_and_belongs_to_many :events
validates :email, presence: true, uniqueness: true
+ validates :password_digest, presence: true
+
+ normalizes :email, with: ->(email) { email.strip.downcase }
+
+ has_secure_password
+
+ generates_token_for :password_reset, expires_in: PASSWORD_RESET_EXPIRATION do
+ password_salt&.last(10)
+ end
end
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb
index 5ddef884..251755fe 100644
--- a/app/views/layouts/application.html.erb
+++ b/app/views/layouts/application.html.erb
@@ -1,19 +1,28 @@
-
- RailsWorld
-
- <%= csrf_meta_tags %>
- <%= csp_meta_tag %>
- <%= stylesheet_link_tag "tailwind", "inter-font", "data-turbo-track": "reload" %>
+
+ RailsWorld
+
+ <%= csrf_meta_tags %>
+ <%= csp_meta_tag %>
+ <%= stylesheet_link_tag "tailwind", "inter-font", "data-turbo-track": "reload" %>
- <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
- <%= javascript_importmap_tags %>
-
+ <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
+ <%= javascript_importmap_tags %>
+
-
-
- <%= yield %>
-
-
+
+
+ <%= notice %>
+ <%= alert %>
+ <% if user_signed_in? %>
+ <%= link_to "Edit Password", edit_password_path %>
+ <%= button_to "Log out", session_path, method: :delete %>
+ <% else %>
+ <%= link_to "Sign Up", new_registration_path %>
+ <%= link_to "Log in", new_session_path %>
+ <% end %>
+ <%= yield %>
+
+
diff --git a/app/views/main/index.html.erb b/app/views/main/index.html.erb
new file mode 100644
index 00000000..bae11ff6
--- /dev/null
+++ b/app/views/main/index.html.erb
@@ -0,0 +1 @@
+Homepage
diff --git a/app/views/password_mailer/password_reset.html.erb b/app/views/password_mailer/password_reset.html.erb
new file mode 100644
index 00000000..45954945
--- /dev/null
+++ b/app/views/password_mailer/password_reset.html.erb
@@ -0,0 +1 @@
+<%= link_to "Reset your password", edit_password_reset_url(token: params[:token]) %>
diff --git a/app/views/password_resets/edit.html.erb b/app/views/password_resets/edit.html.erb
new file mode 100644
index 00000000..333edd06
--- /dev/null
+++ b/app/views/password_resets/edit.html.erb
@@ -0,0 +1,23 @@
+Reset Your Password
+
+<%= form_with model: @user, url: password_reset_path(token: params[:token]) do |form| %>
+ <% if form.object.errors.any? %>
+ <% form.object.errors.full_messages.each do |message| %>
+ <%= message %>
+ <% end %>
+ <% end %>
+
+
+ <%= form.label :password %>
+ <%= form.password_field :password %>
+
+
+
+ <%= form.label :password_confirmation %>
+ <%= form.password_field :password_confirmation %>
+
+
+
+ <%= form.submit "Reset Your Password" %>
+
+<% end %>
diff --git a/app/views/password_resets/new.html.erb b/app/views/password_resets/new.html.erb
new file mode 100644
index 00000000..ea64ecff
--- /dev/null
+++ b/app/views/password_resets/new.html.erb
@@ -0,0 +1,12 @@
+Reset Your Password
+
+<%= form_with model: @user, url: password_reset_path do |form| %>
+
+ <%= form.label :email %>
+ <%= form.email_field :email %>
+
+
+
+ <%= form.submit "Reset password" %>
+
+<% end %>
diff --git a/app/views/passwords/edit.html.erb b/app/views/passwords/edit.html.erb
new file mode 100644
index 00000000..7a30272d
--- /dev/null
+++ b/app/views/passwords/edit.html.erb
@@ -0,0 +1,28 @@
+Update Password
+
+<%= form_with model: current_user, url: password_path do |form| %>
+ <% if form.object.errors.any? %>
+ <% form.object.errors.full_messages.each do |message| %>
+ <%= message %>
+ <% end %>
+ <% end %>
+
+
+ <%= form.label :password_challenge, "Current Password" %>
+ <%= form.password_field :password_challenge %>
+
+
+
+ <%= form.label :password, "New Password" %>
+ <%= form.password_field :password %>
+
+
+
+ <%= form.label :password_confirmation %>
+ <%= form.password_field :password_confirmation %>
+
+
+
+ <%= form.submit "Update Password" %>
+
+<% end %>
diff --git a/app/views/registrations/new.html.erb b/app/views/registrations/new.html.erb
new file mode 100644
index 00000000..82160074
--- /dev/null
+++ b/app/views/registrations/new.html.erb
@@ -0,0 +1,28 @@
+Sign Up
+
+<%= form_with model: @user, url: registration_path do |form| %>
+ <% if form.object.errors.any? %>
+ <% form.object.errors.full_messages.each do |message| %>
+ <%= message %>
+ <% end %>
+ <% end %>
+
+
+ <%= form.label :email %>
+ <%= form.email_field :email %>
+
+
+
+ <%= form.label :password %>
+ <%= form.password_field :password %>
+
+
+
+ <%= form.label :password_confirmation %>
+ <%= form.password_field :password_confirmation %>
+
+
+
+ <%= form.submit "Sign Up" %>
+
+<% end %>
diff --git a/app/views/sessions/new.html.erb b/app/views/sessions/new.html.erb
new file mode 100644
index 00000000..426a07a5
--- /dev/null
+++ b/app/views/sessions/new.html.erb
@@ -0,0 +1,19 @@
+Log in
+
+<%= form_with model: @user, url: session_path do |form| %>
+
+ <%= form.label :email %>
+ <%= form.email_field :email %>
+
+
+
+ <%= form.label :password %>
+ <%= form.password_field :password %>
+
+
+
+ <%= form.submit "Log in" %>
+
+<% end %>
+
+<%= link_to "Forgot your password?", new_password_reset_path %>
diff --git a/config/environments/development.rb b/config/environments/development.rb
index e01622cf..d6d3d088 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -46,6 +46,8 @@
config.action_mailer.perform_caching = false
+ config.action_mailer.default_url_options = {host: "localhost", port: 3000}
+
# Print deprecation notices to the Rails logger.
config.active_support.deprecation = :log
diff --git a/config/environments/test.rb b/config/environments/test.rb
index adbb4a6f..3fb3f577 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -43,6 +43,7 @@
# The :test delivery method accumulates sent emails in the
# ActionMailer::Base.deliveries array.
config.action_mailer.delivery_method = :test
+ config.action_mailer.default_url_options = {host: "localhost", port: 3000}
# Print deprecation notices to the stderr.
config.active_support.deprecation = :stderr
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 6c349ae5..e18f993c 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -28,4 +28,23 @@
# enabled: "ON"
en:
- hello: "Hello world"
+ controllers:
+ concerns:
+ authentication:
+ unauthorized: "You must be logged in to do that."
+ password_resets:
+ create:
+ notice: "Check your email to reset your password."
+ update:
+ notice: "Your password has been reset successfully. Please login."
+ errors:
+ invalid_token: "Invalid token, please try again"
+ passwords:
+ update:
+ notice: "Your password has been updated successfully."
+ sessions:
+ create:
+ notice: "You have signed successfully."
+ alert: "Invalid email or password."
+ destroy:
+ notice: "You have been logged out."
\ No newline at end of file
diff --git a/config/routes.rb b/config/routes.rb
index 4283b751..96a018c0 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -4,5 +4,10 @@
# TODO: authenticate with admin user
mount MissionControl::Jobs::Engine, at: "/jobs"
- # root "posts#index"
+ root "main#index"
+
+ resource :registration, only: [:new, :create]
+ resource :session, only: [:new, :create, :destroy]
+ resource :password, only: [:edit, :update]
+ resource :password_reset, only: [:new, :create, :edit, :update]
end
diff --git a/db/migrate/20240628211820_create_users.rb b/db/migrate/20240628211820_create_users.rb
index 05b0f086..be93835d 100644
--- a/db/migrate/20240628211820_create_users.rb
+++ b/db/migrate/20240628211820_create_users.rb
@@ -5,6 +5,7 @@ def change
t.string :role
t.boolean :mail_notifications_enabled, default: true, null: false
t.boolean :in_app_notifications_enabled, default: true, null: false
+ t.string :password_digest, null: false
t.timestamps
diff --git a/db/migrate/20240703041238_add_index_to_user_email.rb b/db/migrate/20240703041238_add_index_to_user_email.rb
new file mode 100644
index 00000000..ff03658d
--- /dev/null
+++ b/db/migrate/20240703041238_add_index_to_user_email.rb
@@ -0,0 +1,5 @@
+class AddIndexToUserEmail < ActiveRecord::Migration[7.1]
+ def change
+ add_index :users, :email, unique: true
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index c249095a..a6e60d62 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[7.1].define(version: 2024_06_28_211903) do
+ActiveRecord::Schema[7.1].define(version: 2024_07_03_041238) do
create_table "conferences", force: :cascade do |t|
t.string "name", null: false
t.datetime "created_at", null: false
@@ -101,6 +101,7 @@
t.string "role"
t.boolean "mail_notifications_enabled", default: true, null: false
t.boolean "in_app_notifications_enabled", default: true, null: false
+ t.string "password_digest", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["email"], name: "index_users_on_email", unique: true
diff --git a/spec/controllers/password_resets_controller_spec.rb b/spec/controllers/password_resets_controller_spec.rb
new file mode 100644
index 00000000..bb2ec1c6
--- /dev/null
+++ b/spec/controllers/password_resets_controller_spec.rb
@@ -0,0 +1,115 @@
+# frozen_string_literal: true
+
+require "rails_helper"
+
+RSpec.describe PasswordResetsController, type: :controller do
+ let!(:user) { create(:user, password: "password") }
+ let(:user_token) { user.generate_token_for(:password_reset) }
+
+ describe "GET #new" do
+ it "returns a success response" do
+ get :new
+ expect(response).to have_http_status(:ok)
+ end
+ end
+
+ describe "POST #create" do
+ context "with valid params" do
+ let(:params) do
+ {
+ email: user.email
+ }
+ end
+
+ it "sends a password reset email" do
+ expect { post :create, params: params }.to change { ActionMailer::Base.deliveries.count }.by(1)
+ expect(response).to redirect_to(root_path)
+ end
+ end
+ end
+
+ describe "GET #edit" do
+ let(:current) { instance_double(Current) }
+
+ before do
+ allow(Current).to receive(:user).and_return(user)
+ end
+
+ context "with valid token" do
+ it "returns a success response" do
+ get :edit, params: {token: user_token}
+ expect(response).to have_http_status(:ok)
+ expect(assigns(:user)).to eq(user)
+ end
+ end
+
+ context "with invalid token" do
+ it "redirects to new password reset path" do
+ get :edit, params: {token: "invalid_token"}
+ expect(response).to redirect_to(new_password_reset_path(alert: I18n.t("controllers.password_resets.errors.invalid_token")))
+ end
+ end
+ end
+
+ describe "PUT #update" do
+ let(:new_password) { "new_password" }
+ let(:current) { instance_double(Current) }
+
+ before do
+ allow(Current).to receive(:user).and_return(user)
+ end
+
+ context "with valid params" do
+ let(:params) do
+ {
+ user: {
+ password: new_password,
+ password_confirmation: new_password
+ },
+ token: user_token
+ }
+ end
+
+ it "resets user password" do
+ expect { put :update, params: params }
+ .to change { user.reload.authenticate(new_password) }.from(false).to(user)
+ expect(response).to redirect_to(new_session_path)
+ end
+ end
+
+ context "with invalid params" do
+ let(:params) do
+ {
+ user: {
+ password: "password",
+ password_confirmation: "wrong_password"
+ },
+ token: user_token
+ }
+ end
+
+ it "does not update the user password" do
+ expect { put :update, params: params }.not_to change { user.reload.password_digest }
+ expect(response).to have_http_status(:unprocessable_content)
+ expect(response).to render_template(:edit)
+ end
+ end
+
+ context "with invalid token" do
+ let(:params) do
+ {
+ user: {
+ password: "password",
+ password_confirmation: "password"
+ },
+ token: "invalid_token"
+ }
+ end
+
+ it "does not update the user password" do
+ expect { put :update, params: params }.not_to change { user.reload.password_digest }
+ expect(response).to redirect_to(new_password_reset_path(alert: I18n.t("controllers.password_resets.errors.invalid_token")))
+ end
+ end
+ end
+end
diff --git a/spec/controllers/passwords_controller_spec.rb b/spec/controllers/passwords_controller_spec.rb
new file mode 100644
index 00000000..9ad546b1
--- /dev/null
+++ b/spec/controllers/passwords_controller_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require "rails_helper"
+
+RSpec.describe PasswordsController, type: :controller do
+ let!(:user) { create(:user, password: "password") }
+ let(:current) { instance_double(Current) }
+
+ before do
+ allow(Current).to receive(:user).and_return(user)
+ end
+
+ describe "GET #edit" do
+ it "returns a success response" do
+ get :edit
+ expect(response).to have_http_status(:ok)
+ end
+ end
+
+ describe "PUT #update" do
+ let(:new_password) { "new_password" }
+
+ context "with valid params" do
+ let(:params) do
+ {
+ user: {
+ password: new_password,
+ password_confirmation: new_password,
+ password_challenge: "password"
+ }
+ }
+ end
+
+ it "resets user password" do
+ expect { put :update, params: params }.to change { user.reload.authenticate(new_password) }.from(false).to(user)
+ expect(response).to redirect_to(edit_password_path)
+ end
+ end
+
+ context "with invalid params" do
+ let(:params) do
+ {
+ user: {
+ password: "password",
+ password_confirmation: "password",
+ password_challenge: "wrong_password"
+ }
+ }
+ end
+
+ it "does not update the user password" do
+ expect { put :update, params: params }.not_to change { user.reload.password_digest }
+ expect(response).to have_http_status(:unprocessable_content)
+ expect(response).to render_template(:edit)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb
new file mode 100644
index 00000000..fa0f0318
--- /dev/null
+++ b/spec/controllers/registrations_controller_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require "rails_helper"
+
+RSpec.describe RegistrationsController, type: :controller do
+ describe "GET #new" do
+ it "returns a success response" do
+ get :new
+ expect(response).to have_http_status(:ok)
+ assigns(:user).should be_a_new(User)
+ end
+ end
+
+ describe "POST #create" do
+ context "with valid params" do
+ let(:params) do
+ {
+ user: {
+ email: "dev@example.com",
+ password: "password",
+ password_confirmation: "password"
+ }
+ }
+ end
+
+ it "creates a new User" do
+ expect { post :create, params: params }.to change(User, :count).by(1)
+ expect(session[:user_id]).to eq(User.last.id)
+ expect(response).to redirect_to(root_path)
+ end
+ end
+
+ context "with invalid params" do
+ let(:params) { {user: {email: ""}} }
+
+ it "does not create a new User" do
+ expect { post :create, params: params }.not_to change(User, :count)
+ expect(response).to have_http_status(:unprocessable_content)
+ expect(response).to render_template(:new)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb
new file mode 100644
index 00000000..31e8b614
--- /dev/null
+++ b/spec/controllers/sessions_controller_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require "rails_helper"
+
+RSpec.describe SessionsController, type: :controller do
+ describe "GET #new" do
+ it "returns a success response" do
+ get :new
+ expect(response).to have_http_status(:ok)
+ end
+ end
+
+ describe "POST #create" do
+ let!(:user) { create(:user, password: "password") }
+
+ context "with valid params" do
+ let(:params) do
+ {
+ user: {
+ email: user.email,
+ password: "password"
+ }
+ }
+ end
+
+ it "creates a User session" do
+ post :create, params: params
+ expect(session[:user_id]).to eq(user.id)
+ expect(response).to redirect_to(root_path)
+ assigns(:user).should eq(user)
+ end
+ end
+
+ context "with invalid params" do
+ let(:params) do
+ {
+ user: {
+ email: user.email,
+ password: "invalid_password"
+ }
+ }
+ end
+
+ it "does not create a User session" do
+ expect { post :create, params: params }.not_to change(User, :count)
+ expect(response).to have_http_status(:unprocessable_content)
+ expect(response).to render_template(:new)
+ end
+ end
+ end
+end
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
index 2c70c118..bebbe6aa 100644
--- a/spec/factories/users.rb
+++ b/spec/factories/users.rb
@@ -16,7 +16,8 @@
#
FactoryBot.define do
factory :user do
- email { "dev@example.com" }
+ email { Faker::Internet.email }
+ password { "password" }
trait :with_profile do
profile
diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb
index 12bcb8c5..366e19e9 100644
--- a/spec/rails_helper.rb
+++ b/spec/rails_helper.rb
@@ -58,4 +58,5 @@
config.filter_rails_from_backtrace!
# arbitrary gems may also be filtered via:
# config.filter_gems_from_backtrace("gem name")
+ config.include FactoryBot::Syntax::Methods
end