diff --git a/.env.template b/.env.template index c80d0eff..0e694f27 100644 --- a/.env.template +++ b/.env.template @@ -1,5 +1,6 @@ OVERMIND_PROCFILE=Procfile.dev HIVEMIND_PROCFILE=Procfile.dev +PORT=3000 OVERMIND_ENV=./.env.local HIVEMIND_ENV=./.env.local RSPEC_RETRY_RETRY_COUNT=0 diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml index 4ed4df5d..e955b403 100644 --- a/.github/workflows/github-actions.yml +++ b/.github/workflows/github-actions.yml @@ -86,6 +86,8 @@ jobs: - name: Setup DB run: | bundle exec rails db:test:prepare + - name: Assets Precompile + run: bundle exec rails assets:precompile - name: Run tests run: | bundle exec rspec diff --git a/app/controllers/concerns/authentication.rb b/app/controllers/concerns/authentication.rb index 55400474..6414e533 100644 --- a/app/controllers/concerns/authentication.rb +++ b/app/controllers/concerns/authentication.rb @@ -4,25 +4,31 @@ module Authentication included do helper_method :current_user, :user_signed_in? - before_action :authenticate_user! + before_action :authenticate_user, :authenticate_user! end - def authenticate_user! - return current_user if user_signed_in? - - redirect_to root_path, alert: t("controllers.concerns.authentication.unauthorized") + class_methods do + def allow_unauthenticated_access(**options) + skip_before_action :authenticate_user!, **options + end end - def current_user - Current.user ||= authenticate_user_from_session - end + private + + def current_user = Current.user - def authenticate_user_from_session - User.find_by(id: session[:user_id]) + def user_signed_in? = Current.user.present? + + def authenticate_user + Current.user = User.find_by(id: session[:user_id]) end - def user_signed_in? - current_user.present? + def authenticate_user! + authenticate_user + + if !user_signed_in? + redirect_to new_session_path, alert: t("controllers.concerns.authentication.unauthorized") + end end def login(user) diff --git a/app/controllers/main_controller.rb b/app/controllers/main_controller.rb index f6b17f11..c98a8eaa 100644 --- a/app/controllers/main_controller.rb +++ b/app/controllers/main_controller.rb @@ -1,5 +1,6 @@ class MainController < ApplicationController - skip_before_action :authenticate_user! + allow_unauthenticated_access + def index end end diff --git a/app/controllers/password_resets_controller.rb b/app/controllers/password_resets_controller.rb index 15f94e59..8e4429f4 100644 --- a/app/controllers/password_resets_controller.rb +++ b/app/controllers/password_resets_controller.rb @@ -1,5 +1,5 @@ class PasswordResetsController < ApplicationController - skip_before_action :authenticate_user!, only: [:new, :create] + allow_unauthenticated_access before_action :set_user_by_token, only: [:edit, :update] @@ -19,7 +19,7 @@ def create ).password_reset.deliver_later end - redirect_to root_path, notice: t("controllers.password_resets.create.notice") + redirect_to new_session_path, notice: t("controllers.password_resets.create.notice") end def update @@ -36,7 +36,7 @@ def set_user_by_token @user = User.find_by_token_for(:password_reset, params[:token]) return if @user.present? - redirect_to new_password_reset_path alert: t("controllers.password_resets.errors.invalid_token") + redirect_to new_password_reset_path, alert: t("controllers.password_resets.errors.invalid_token") end def password_params diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb index 82969ec9..1930c97b 100644 --- a/app/controllers/passwords_controller.rb +++ b/app/controllers/passwords_controller.rb @@ -3,7 +3,7 @@ def edit end def update - if current_user.update(password_params) + if Current.user.update(password_params) redirect_to edit_password_path, notice: t("controllers.passwords.update.notice") else render :edit, status: :unprocessable_entity diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 199ba626..37651612 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -1,5 +1,5 @@ class RegistrationsController < ApplicationController - skip_before_action :authenticate_user! + allow_unauthenticated_access def new @user = User.new diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 00e79feb..fae2553e 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -1,5 +1,5 @@ class SessionsController < ApplicationController - skip_before_action :authenticate_user! + allow_unauthenticated_access only: [:new, :create] def new @user = User.new @@ -12,8 +12,7 @@ def create 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 + redirect_to new_session_path, alert: t("controllers.sessions.create.alert") end end diff --git a/app/models/user.rb b/app/models/user.rb index 86e22a32..8b641a70 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -28,6 +28,7 @@ class User < ApplicationRecord validates :email, presence: true, uniqueness: true validates :password_digest, presence: true + validates :password, length: {minimum: 8}, if: -> { password.present? } generates_token_for :password_reset, expires_in: PASSWORD_RESET_EXPIRATION do password_salt&.last(10) diff --git a/app/views/passwords/edit.html.erb b/app/views/passwords/edit.html.erb index 7a30272d..b1959002 100644 --- a/app/views/passwords/edit.html.erb +++ b/app/views/passwords/edit.html.erb @@ -1,6 +1,6 @@

Update Password

-<%= form_with model: current_user, url: password_path do |form| %> +<%= form_with model: Current.user, url: password_path do |form| %> <% if form.object.errors.any? %> <% form.object.errors.full_messages.each do |message| %>
<%= message %>
diff --git a/app/views/registrations/new.html.erb b/app/views/registrations/new.html.erb index 82160074..f1a6c724 100644 --- a/app/views/registrations/new.html.erb +++ b/app/views/registrations/new.html.erb @@ -9,20 +9,20 @@
<%= form.label :email %> - <%= form.email_field :email %> + <%= form.email_field :email, data: { test_id: "email_field" } %>
<%= form.label :password %> - <%= form.password_field :password %> + <%= form.password_field :password, data: { test_id: "password_field" } %>
<%= form.label :password_confirmation %> - <%= form.password_field :password_confirmation %> + <%= form.password_field :password_confirmation, data: { test_id: "password_confirmation_field" } %>
- <%= form.submit "Sign Up" %> + <%= form.submit "Sign Up", data: { test_id: "sign_up_button" } %>
<% end %> diff --git a/app/views/sessions/new.html.erb b/app/views/sessions/new.html.erb index 426a07a5..ef79ab62 100644 --- a/app/views/sessions/new.html.erb +++ b/app/views/sessions/new.html.erb @@ -3,16 +3,16 @@ <%= form_with model: @user, url: session_path do |form| %>
<%= form.label :email %> - <%= form.email_field :email %> + <%= form.email_field :email, data: { test_id: "email_field" } %>
<%= form.label :password %> - <%= form.password_field :password %> + <%= form.password_field :password, data: { test_id: "password_field" } %>
- <%= form.submit "Log in" %> + <%= form.submit "Log in", data: { test_id: "sign_in_button" } %>
<% end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index e18f993c..cd38512d 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1,37 +1,8 @@ -# Files in the config/locales directory are used for internationalization and -# are automatically loaded by Rails. If you want to use locales other than -# English, add the necessary files in this directory. -# -# To use the locales, use `I18n.t`: -# -# I18n.t "hello" -# -# In views, this is aliased to just `t`: -# -# <%= t("hello") %> -# -# To use a different locale, set it with `I18n.locale`: -# -# I18n.locale = :es -# -# This would use the information in config/locales/es.yml. -# -# To learn more about the API, please read the Rails Internationalization guide -# at https://guides.rubyonrails.org/i18n.html. -# -# Be aware that YAML interprets the following case-insensitive strings as -# booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings -# must be quoted to be interpreted as strings. For example: -# -# en: -# "yes": yup -# enabled: "ON" - en: controllers: concerns: authentication: - unauthorized: "You must be logged in to do that." + unauthorized: "You need to sign in or sign up before continuing." password_resets: create: notice: "Check your email to reset your password." @@ -44,7 +15,7 @@ en: notice: "Your password has been updated successfully." sessions: create: - notice: "You have signed successfully." + notice: "Signed in successfully." alert: "Invalid email or password." destroy: - notice: "You have been logged out." \ No newline at end of file + notice: "You have been logged out." diff --git a/db/seeds.rb b/db/seeds.rb index cee8acc9..16de4ebb 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,7 +1,7 @@ conference = Conference.find_or_create_by!(name: "RailsWorld 2024") # Users -user = User.create!(email: "dev@example.com", password: "foobar", password_confirmation: "foobar") +user = User.create!(email: "dev@example.com", password: "foobar2024", password_confirmation: "foobar2024") # Tags Tag.create!(name: "Hotwire") diff --git a/spec/controllers/password_resets_controller_spec.rb b/spec/controllers/password_resets_controller_spec.rb index bb2ec1c6..bd7dfe3a 100644 --- a/spec/controllers/password_resets_controller_spec.rb +++ b/spec/controllers/password_resets_controller_spec.rb @@ -22,17 +22,17 @@ 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) + expect { + post :create, params: params + }.to have_enqueued_mail(PasswordMailer, :password_reset) + expect(response).to redirect_to(new_session_path) end end end describe "GET #edit" do - let(:current) { instance_double(Current) } - before do - allow(Current).to receive(:user).and_return(user) + sign_in(user) end context "with valid token" do @@ -46,17 +46,16 @@ 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"))) + expect(response).to redirect_to(new_password_reset_path) 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) + sign_in(user) end context "with valid params" do @@ -108,7 +107,7 @@ 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"))) + expect(response).to redirect_to(new_password_reset_path) end end end diff --git a/spec/controllers/passwords_controller_spec.rb b/spec/controllers/passwords_controller_spec.rb index 9ad546b1..e3b29a21 100644 --- a/spec/controllers/passwords_controller_spec.rb +++ b/spec/controllers/passwords_controller_spec.rb @@ -4,10 +4,9 @@ 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) + sign_in(user) end describe "GET #edit" do diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb index fa0f0318..c2556edd 100644 --- a/spec/controllers/registrations_controller_spec.rb +++ b/spec/controllers/registrations_controller_spec.rb @@ -7,7 +7,6 @@ it "returns a success response" do get :new expect(response).to have_http_status(:ok) - assigns(:user).should be_a_new(User) end end diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb index 31e8b614..55cb7628 100644 --- a/spec/controllers/sessions_controller_spec.rb +++ b/spec/controllers/sessions_controller_spec.rb @@ -27,7 +27,6 @@ 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 @@ -43,8 +42,8 @@ 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) + expect(response).to have_http_status(:found) + expect(response).to redirect_to(new_session_path) end end end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index 14539a09..7f25f9e4 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -18,7 +18,7 @@ FactoryBot.define do factory :user do email { Faker::Internet.email } - password { "password" } + password { "password2024" } trait :with_profile do profile diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 366e19e9..b23e95a3 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -30,6 +30,8 @@ abort e.to_s.strip end RSpec.configure do |config| + ActiveJob::Base.queue_adapter = :test + config.include FactoryBot::Syntax::Methods # If you're not using ActiveRecord, or you'd prefer not to run each of your # examples within a transaction, remove the following line or assign false @@ -58,5 +60,4 @@ 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 diff --git a/spec/support/authentication_helper.rb b/spec/support/authentication_helper.rb new file mode 100644 index 00000000..ed2091c2 --- /dev/null +++ b/spec/support/authentication_helper.rb @@ -0,0 +1,17 @@ +module AuthenticationHelper + def sign_in(user, password = "password2024") + if respond_to?(:visit) # System specs + visit new_session_path + find_dti("email_field").set(user.email) + find_dti("password_field").set(password) + find_dti("sign_in_button").click + else # Controller specs + session[:user_id] = user.id + end + end +end + +RSpec.configure do |config| + config.include AuthenticationHelper, type: :system + config.include AuthenticationHelper, type: :controller +end diff --git a/spec/system/test_spec.rb b/spec/system/test_spec.rb deleted file mode 100644 index 2510aef5..00000000 --- a/spec/system/test_spec.rb +++ /dev/null @@ -1,8 +0,0 @@ -require "rails_helper" - -RSpec.describe "System specs health check", type: :system do - it "website is up" do - visit "/up" - expect(page).to have_http_status(:ok) - end -end diff --git a/spec/system/user_sign_in_spec.rb b/spec/system/user_sign_in_spec.rb new file mode 100644 index 00000000..62f87f69 --- /dev/null +++ b/spec/system/user_sign_in_spec.rb @@ -0,0 +1,37 @@ +require "rails_helper" + +RSpec.describe "User sign in", type: :system do + let!(:user) { create(:user, email: "test@test.com", password: "foobar2024") } + + it "redirects to the login page when trying to access another page" do + visit edit_password_path + expect(page).to have_current_path(new_session_path) + expect(page).to have_content("You need to sign in or sign up before continuing.") + end + + context "when the user inputs invalid credentials" do + it "does not sign the user in" do + visit new_session_path + find_dti("email_field").set("test@test.com") + find_dti("password_field").set("wrongpassword") + find_dti("sign_in_button").click + expect(page).to have_content("Invalid email or password.") + + visit edit_password_path + expect(page).to have_current_path(new_session_path) + end + end + + context "when the user inputs valid credentials" do + it "signs the user in" do + visit new_session_path + find_dti("email_field").set("test@test.com") + find_dti("password_field").set("foobar2024") + find_dti("sign_in_button").click + expect(page).to have_content("Signed in successfully.") + + visit edit_password_path + expect(page).to have_current_path(edit_password_path) + end + end +end diff --git a/spec/system/user_sign_up_spec.rb b/spec/system/user_sign_up_spec.rb new file mode 100644 index 00000000..8d3915fe --- /dev/null +++ b/spec/system/user_sign_up_spec.rb @@ -0,0 +1,52 @@ +require "rails_helper" + +RSpec.describe "User sign up", type: :system do + before { visit new_registration_path } + + context "when the password & password confirmation doesn't match up" do + it "does not create a new user" do + find_dti("email_field").set("test@test.com") + find_dti("password_field").set("hello") + find_dti("password_confirmation_field").set("world") + find_dti("sign_up_button").click + + expect(page).to have_content("Password confirmation doesn't match Password") + end + end + + context "when the email is already taken" do + let!(:user) { create(:user, email: "test@test.com") } + + it "does not create a new user" do + find_dti("email_field").set("test@test.com") + find_dti("password_field").set("foobar2024") + find_dti("password_confirmation_field").set("foobar2024") + find_dti("sign_up_button").click + + expect(page).to have_content("Email has already been taken") + end + end + + context "when the password doesn't met the length criteria" do + it "does not create a new user" do + find_dti("email_field").set("test@test.com") + find_dti("password_field").set("foobar") + find_dti("password_confirmation_field").set("foobar") + find_dti("sign_up_button").click + + expect(page).to have_content("Password is too short (minimum is 8 characters)") + end + end + + context "when the user inputs valid credentials" do + it "creates a new user" do + find_dti("email_field").set("test@test.com") + find_dti("password_field").set("foobar2024") + find_dti("password_confirmation_field").set("foobar2024") + find_dti("sign_up_button").click + + visit edit_password_path + expect(page).to have_current_path(edit_password_path) + end + end +end