diff --git a/Gemfile b/Gemfile index 1a96160..9f167bc 100644 --- a/Gemfile +++ b/Gemfile @@ -57,3 +57,7 @@ group :test do gem "capybara" gem "selenium-webdriver" end + +gem "authentication-zero", "~> 3.0" +# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] +gem "bcrypt", "~> 3.1.7" diff --git a/Gemfile.lock b/Gemfile.lock index bc9361f..a4f0d8a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -74,7 +74,9 @@ GEM addressable (2.8.7) public_suffix (>= 2.0.2, < 7.0) ast (2.4.2) + authentication-zero (3.0.2) base64 (0.2.0) + bcrypt (3.1.20) bigdecimal (3.1.8) bindex (0.8.1) bootsnap (1.18.4) @@ -321,6 +323,8 @@ PLATFORMS x86_64-linux-musl DEPENDENCIES + authentication-zero (~> 3.0) + bcrypt (~> 3.1.7) bootsnap brakeman capybara diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 0d95db2..27fa7bd 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,4 +1,18 @@ class ApplicationController < ActionController::Base - # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has. - allow_browser versions: :modern + before_action :set_current_request_details + before_action :authenticate + + private + def authenticate + if session_record = Session.find_by_id(cookies.signed[:session_token]) + Current.session = session_record + else + redirect_to sign_in_path + end + end + + def set_current_request_details + Current.user_agent = request.user_agent + Current.ip_address = request.ip + end end diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb new file mode 100644 index 0000000..95f2992 --- /dev/null +++ b/app/controllers/home_controller.rb @@ -0,0 +1,4 @@ +class HomeController < ApplicationController + def index + end +end diff --git a/app/controllers/identity/email_verifications_controller.rb b/app/controllers/identity/email_verifications_controller.rb new file mode 100644 index 0000000..5c48dc1 --- /dev/null +++ b/app/controllers/identity/email_verifications_controller.rb @@ -0,0 +1,26 @@ +class Identity::EmailVerificationsController < ApplicationController + skip_before_action :authenticate, only: :show + + before_action :set_user, only: :show + + def show + @user.update! verified: true + redirect_to root_path, notice: "Thank you for verifying your email address" + end + + def create + send_email_verification + redirect_to root_path, notice: "We sent a verification email to your email address" + end + + private + def set_user + @user = User.find_by_token_for!(:email_verification, params[:sid]) + rescue StandardError + redirect_to edit_identity_email_path, alert: "That email verification link is invalid" + end + + def send_email_verification + UserMailer.with(user: Current.user).email_verification.deliver_later + end +end diff --git a/app/controllers/identity/emails_controller.rb b/app/controllers/identity/emails_controller.rb new file mode 100644 index 0000000..407e388 --- /dev/null +++ b/app/controllers/identity/emails_controller.rb @@ -0,0 +1,36 @@ +class Identity::EmailsController < ApplicationController + before_action :set_user + + def edit + end + + def update + if @user.update(user_params) + redirect_to_root + else + render :edit, status: :unprocessable_entity + end + end + + private + def set_user + @user = Current.user + end + + def user_params + params.permit(:email, :password_challenge).with_defaults(password_challenge: "") + end + + def redirect_to_root + if @user.email_previously_changed? + resend_email_verification + redirect_to root_path, notice: "Your email has been changed" + else + redirect_to root_path + end + end + + def resend_email_verification + UserMailer.with(user: @user).email_verification.deliver_later + end +end diff --git a/app/controllers/identity/password_resets_controller.rb b/app/controllers/identity/password_resets_controller.rb new file mode 100644 index 0000000..44583c1 --- /dev/null +++ b/app/controllers/identity/password_resets_controller.rb @@ -0,0 +1,43 @@ +class Identity::PasswordResetsController < ApplicationController + skip_before_action :authenticate + + before_action :set_user, only: %i[ edit update ] + + def new + end + + def edit + end + + def create + if @user = User.find_by(email: params[:email], verified: true) + send_password_reset_email + redirect_to sign_in_path, notice: "Check your email for reset instructions" + else + redirect_to new_identity_password_reset_path, alert: "You can't reset your password until you verify your email" + end + end + + def update + if @user.update(user_params) + redirect_to sign_in_path, notice: "Your password was reset successfully. Please sign in" + else + render :edit, status: :unprocessable_entity + end + end + + private + def set_user + @user = User.find_by_token_for!(:password_reset, params[:sid]) + rescue StandardError + redirect_to new_identity_password_reset_path, alert: "That password reset link is invalid" + end + + def user_params + params.permit(:password, :password_confirmation) + end + + def send_password_reset_email + UserMailer.with(user: @user).password_reset.deliver_later + end +end diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb new file mode 100644 index 0000000..1956a8b --- /dev/null +++ b/app/controllers/passwords_controller.rb @@ -0,0 +1,23 @@ +class PasswordsController < ApplicationController + before_action :set_user + + def edit + end + + def update + if @user.update(user_params) + redirect_to root_path, notice: "Your password has been changed" + else + render :edit, status: :unprocessable_entity + end + end + + private + def set_user + @user = Current.user + end + + def user_params + params.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 0000000..1126761 --- /dev/null +++ b/app/controllers/registrations_controller.rb @@ -0,0 +1,30 @@ +class RegistrationsController < ApplicationController + skip_before_action :authenticate + + def new + @user = User.new + end + + def create + @user = User.new(user_params) + + if @user.save + session_record = @user.sessions.create! + cookies.signed.permanent[:session_token] = { value: session_record.id, httponly: true } + + send_email_verification + redirect_to root_path, notice: "Welcome! You have signed up successfully" + else + render :new, status: :unprocessable_entity + end + end + + private + def user_params + params.permit(:email, :password, :password_confirmation) + end + + def send_email_verification + UserMailer.with(user: @user).email_verification.deliver_later + end +end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb new file mode 100644 index 0000000..3abf69f --- /dev/null +++ b/app/controllers/sessions_controller.rb @@ -0,0 +1,32 @@ +class SessionsController < ApplicationController + skip_before_action :authenticate, only: %i[ new create ] + + before_action :set_session, only: :destroy + + def index + @sessions = Current.user.sessions.order(created_at: :desc) + end + + def new + end + + def create + if user = User.authenticate_by(email: params[:email], password: params[:password]) + @session = user.sessions.create! + cookies.signed.permanent[:session_token] = { value: @session.id, httponly: true } + + redirect_to root_path, notice: "Signed in successfully" + else + redirect_to sign_in_path(email_hint: params[:email]), alert: "That email or password is incorrect" + end + end + + def destroy + @session.destroy; redirect_to(sessions_path, notice: "That session has been logged out") + end + + private + def set_session + @session = Current.user.sessions.find(params[:id]) + end +end diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb new file mode 100644 index 0000000..98c2045 --- /dev/null +++ b/app/mailers/user_mailer.rb @@ -0,0 +1,15 @@ +class UserMailer < ApplicationMailer + def password_reset + @user = params[:user] + @signed_id = @user.generate_token_for(:password_reset) + + mail to: @user.email, subject: "Reset your password" + end + + def email_verification + @user = params[:user] + @signed_id = @user.generate_token_for(:email_verification) + + mail to: @user.email, subject: "Verify your email" + end +end diff --git a/app/models/current.rb b/app/models/current.rb new file mode 100644 index 0000000..0d0863f --- /dev/null +++ b/app/models/current.rb @@ -0,0 +1,6 @@ +class Current < ActiveSupport::CurrentAttributes + attribute :session + attribute :user_agent, :ip_address + + delegate :user, to: :session, allow_nil: true +end diff --git a/app/models/session.rb b/app/models/session.rb new file mode 100644 index 0000000..8e94aa8 --- /dev/null +++ b/app/models/session.rb @@ -0,0 +1,8 @@ +class Session < ApplicationRecord + belongs_to :user + + before_create do + self.user_agent = Current.user_agent + self.ip_address = Current.ip_address + end +end diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100644 index 0000000..20ccbba --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,26 @@ +class User < ApplicationRecord + has_secure_password + + generates_token_for :email_verification, expires_in: 2.days do + email + end + generates_token_for :password_reset, expires_in: 20.minutes do + password_salt.last(10) + end + + + has_many :sessions, dependent: :destroy + + validates :email, presence: true, uniqueness: true, format: { with: URI::MailTo::EMAIL_REGEXP } + validates :password, allow_nil: true, length: { minimum: 12 } + + normalizes :email, with: -> { _1.strip.downcase } + + before_validation if: :email_changed?, on: :update do + self.verified = false + end + + after_update if: :password_digest_previously_changed? do + sessions.where.not(id: Current.session).delete_all + end +end diff --git a/app/views/home/index.html.erb b/app/views/home/index.html.erb new file mode 100644 index 0000000..677883d --- /dev/null +++ b/app/views/home/index.html.erb @@ -0,0 +1,23 @@ +
<%= notice %>
+ +Signed as <%= Current.user.email %>
+ +<%= alert %>
+ +<% if Current.user.verified? %> +We sent a verification email to the address below. Check that email and follow those instructions to confirm it's your email address.
+<%= button_to "Re-send verification email", identity_email_verification_path %>
+<% end %> + +<%= form_with(url: identity_email_path, method: :patch) do |form| %> + <% if @user.errors.any? %> +<%= alert %>
+ +<%= alert %>
+ +<%= notice %>
+ ++ User Agent: + <%= session.user_agent %> +
+ ++ Ip Address: + <%= session.ip_address %> +
+ ++ Created at: + <%= session.created_at %> +
+ ++ <%= button_to "Log out", session, method: :delete %> +
+ <% end %> +<%= notice %>
+<%= alert %>
+ +Hey there,
+ +This is to confirm that <%= @user.email %> is the email you want to use on your account. If you ever lose your password, that's where we'll email a reset link.
+ +You must hit the link below to confirm that you received this email.
+ +<%= link_to "Yes, use this email for my account", identity_email_verification_url(sid: @signed_id) %>
+ +Have questions or need help? Just reply to this email and our support team will help you sort it out.
diff --git a/app/views/user_mailer/password_reset.html.erb b/app/views/user_mailer/password_reset.html.erb new file mode 100644 index 0000000..c570a0a --- /dev/null +++ b/app/views/user_mailer/password_reset.html.erb @@ -0,0 +1,11 @@ +Hey there,
+ +Can't remember your password for <%= @user.email %>? That's OK, it happens. Just hit the link below to set a new one.
+ +<%= link_to "Reset my password", edit_identity_password_reset_url(sid: @signed_id) %>
+ +If you did not request a password reset you can safely ignore this email, it expires in 20 minutes. Only someone with access to this email account can reset your password.
+ +Have questions or need help? Just reply to this email and our support team will help you sort it out.
diff --git a/config/environments/test.rb b/config/environments/test.rb index 0c616a1..f62240e 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -6,6 +6,7 @@ require "active_support/core_ext/integer/time" # and recreated between test runs. Don't rely on the data there! Rails.application.configure do + config.action_mailer.default_url_options = { host: "localhost", port: 3000 } # Settings specified here will take precedence over those in config/application.rb. # While tests run files are not watched, reloading is not necessary. diff --git a/config/routes.rb b/config/routes.rb index 33c9639..74131bd 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,4 +1,16 @@ Rails.application.routes.draw do + get "sign_in", to: "sessions#new" + post "sign_in", to: "sessions#create" + get "sign_up", to: "registrations#new" + post "sign_up", to: "registrations#create" + resources :sessions, only: [:index, :show, :destroy] + resource :password, only: [:edit, :update] + namespace :identity do + resource :email, only: [:edit, :update] + resource :email_verification, only: [:show, :create] + resource :password_reset, only: [:new, :edit, :create, :update] + end + root "home#index" # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. diff --git a/db/migrate/20240817191344_create_users.rb b/db/migrate/20240817191344_create_users.rb new file mode 100644 index 0000000..2bdd8c1 --- /dev/null +++ b/db/migrate/20240817191344_create_users.rb @@ -0,0 +1,12 @@ +class CreateUsers < ActiveRecord::Migration[7.2] + def change + create_table :users do |t| + t.string :email, null: false, index: { unique: true } + t.string :password_digest, null: false + + t.boolean :verified, null: false, default: false + + t.timestamps + end + end +end diff --git a/db/migrate/20240817191345_create_sessions.rb b/db/migrate/20240817191345_create_sessions.rb new file mode 100644 index 0000000..a828909 --- /dev/null +++ b/db/migrate/20240817191345_create_sessions.rb @@ -0,0 +1,11 @@ +class CreateSessions < ActiveRecord::Migration[7.2] + def change + create_table :sessions do |t| + t.references :user, null: false, foreign_key: true + t.string :user_agent + t.string :ip_address + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb new file mode 100644 index 0000000..b67c648 --- /dev/null +++ b/db/schema.rb @@ -0,0 +1,33 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema[7.2].define(version: 2024_08_17_191345) do + create_table "sessions", force: :cascade do |t| + t.integer "user_id", null: false + t.string "user_agent" + t.string "ip_address" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["user_id"], name: "index_sessions_on_user_id" + end + + create_table "users", force: :cascade do |t| + t.string "email", null: false + t.string "password_digest", null: false + t.boolean "verified", default: false, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["email"], name: "index_users_on_email", unique: true + end + + add_foreign_key "sessions", "users" +end diff --git a/test/application_system_test_case.rb b/test/application_system_test_case.rb index cee29fd..f3db1f4 100644 --- a/test/application_system_test_case.rb +++ b/test/application_system_test_case.rb @@ -1,5 +1,15 @@ require "test_helper" class ApplicationSystemTestCase < ActionDispatch::SystemTestCase - driven_by :selenium, using: :headless_chrome, screen_size: [ 1400, 1400 ] + driven_by :selenium, using: :headless_chrome, screen_size: [1400, 1400] + + def sign_in_as(user) + visit sign_in_url + fill_in :email, with: user.email + fill_in :password, with: "Secret1*3*5*" + click_on "Sign in" + + assert_current_path root_url + user + end end diff --git a/test/controllers/identity/email_verifications_controller_test.rb b/test/controllers/identity/email_verifications_controller_test.rb new file mode 100644 index 0000000..90d8513 --- /dev/null +++ b/test/controllers/identity/email_verifications_controller_test.rb @@ -0,0 +1,34 @@ +require "test_helper" + +class Identity::EmailVerificationsControllerTest < ActionDispatch::IntegrationTest + setup do + @user = sign_in_as(users(:lazaro_nixon)) + @user.update! verified: false + end + + test "should send a verification email" do + assert_enqueued_email_with UserMailer, :email_verification, params: { user: @user } do + post identity_email_verification_url + end + + assert_redirected_to root_url + end + + test "should verify email" do + sid = @user.generate_token_for(:email_verification) + + get identity_email_verification_url(sid: sid, email: @user.email) + assert_redirected_to root_url + end + + test "should not verify email with expired token" do + sid = @user.generate_token_for(:email_verification) + + travel 3.days + + get identity_email_verification_url(sid: sid, email: @user.email) + + assert_redirected_to edit_identity_email_url + assert_equal "That email verification link is invalid", flash[:alert] + end +end diff --git a/test/controllers/identity/emails_controller_test.rb b/test/controllers/identity/emails_controller_test.rb new file mode 100644 index 0000000..dc8ecc4 --- /dev/null +++ b/test/controllers/identity/emails_controller_test.rb @@ -0,0 +1,25 @@ +require "test_helper" + +class Identity::EmailsControllerTest < ActionDispatch::IntegrationTest + setup do + @user = sign_in_as(users(:lazaro_nixon)) + end + + test "should get edit" do + get edit_identity_email_url + assert_response :success + end + + test "should update email" do + patch identity_email_url, params: { email: "new_email@hey.com", password_challenge: "Secret1*3*5*" } + assert_redirected_to root_url + end + + + test "should not update email with wrong password challenge" do + patch identity_email_url, params: { email: "new_email@hey.com", password_challenge: "SecretWrong1*3" } + + assert_response :unprocessable_entity + assert_select "li", /Password challenge is invalid/ + end +end diff --git a/test/controllers/identity/password_resets_controller_test.rb b/test/controllers/identity/password_resets_controller_test.rb new file mode 100644 index 0000000..5b807c5 --- /dev/null +++ b/test/controllers/identity/password_resets_controller_test.rb @@ -0,0 +1,65 @@ +require "test_helper" + +class Identity::PasswordResetsControllerTest < ActionDispatch::IntegrationTest + setup do + @user = users(:lazaro_nixon) + end + + test "should get new" do + get new_identity_password_reset_url + assert_response :success + end + + test "should get edit" do + sid = @user.generate_token_for(:password_reset) + + get edit_identity_password_reset_url(sid: sid) + assert_response :success + end + + test "should send a password reset email" do + assert_enqueued_email_with UserMailer, :password_reset, params: { user: @user } do + post identity_password_reset_url, params: { email: @user.email } + end + + assert_redirected_to sign_in_url + end + + test "should not send a password reset email to a nonexistent email" do + assert_no_enqueued_emails do + post identity_password_reset_url, params: { email: "invalid_email@hey.com" } + end + + assert_redirected_to new_identity_password_reset_url + assert_equal "You can't reset your password until you verify your email", flash[:alert] + end + + test "should not send a password reset email to a unverified email" do + @user.update! verified: false + + assert_no_enqueued_emails do + post identity_password_reset_url, params: { email: @user.email } + end + + assert_redirected_to new_identity_password_reset_url + assert_equal "You can't reset your password until you verify your email", flash[:alert] + end + + test "should update password" do + sid = @user.generate_token_for(:password_reset) + + patch identity_password_reset_url, params: { sid: sid, password: "Secret6*4*2*", password_confirmation: "Secret6*4*2*" } + assert_redirected_to sign_in_url + end + + test "should not update password with expired token" do + sid = @user.generate_token_for(:password_reset) + + travel 30.minutes + + patch identity_password_reset_url, params: { sid: sid, password: "Secret6*4*2*", password_confirmation: "Secret6*4*2*" } + + assert_redirected_to new_identity_password_reset_url + assert_equal "That password reset link is invalid", flash[:alert] + end +end diff --git a/test/controllers/passwords_controller_test.rb b/test/controllers/passwords_controller_test.rb new file mode 100644 index 0000000..430f1a1 --- /dev/null +++ b/test/controllers/passwords_controller_test.rb @@ -0,0 +1,24 @@ +require "test_helper" + +class PasswordsControllerTest < ActionDispatch::IntegrationTest + setup do + @user = sign_in_as(users(:lazaro_nixon)) + end + + test "should get edit" do + get edit_password_url + assert_response :success + end + + test "should update password" do + patch password_url, params: { password_challenge: "Secret1*3*5*", password: "Secret6*4*2*", password_confirmation: "Secret6*4*2*" } + assert_redirected_to root_url + end + + test "should not update password with wrong password challenge" do + patch password_url, params: { password_challenge: "SecretWrong1*3", password: "Secret6*4*2*", password_confirmation: "Secret6*4*2*" } + + assert_response :unprocessable_entity + assert_select "li", /Password challenge is invalid/ + end +end diff --git a/test/controllers/registrations_controller_test.rb b/test/controllers/registrations_controller_test.rb new file mode 100644 index 0000000..1636fd8 --- /dev/null +++ b/test/controllers/registrations_controller_test.rb @@ -0,0 +1,16 @@ +require "test_helper" + +class RegistrationsControllerTest < ActionDispatch::IntegrationTest + test "should get new" do + get sign_up_url + assert_response :success + end + + test "should sign up" do + assert_difference("User.count") do + post sign_up_url, params: { email: "lazaronixon@hey.com", password: "Secret1*3*5*", password_confirmation: "Secret1*3*5*" } + end + + assert_redirected_to root_url + end +end diff --git a/test/controllers/sessions_controller_test.rb b/test/controllers/sessions_controller_test.rb new file mode 100644 index 0000000..975c1ca --- /dev/null +++ b/test/controllers/sessions_controller_test.rb @@ -0,0 +1,46 @@ +require "test_helper" + +class SessionsControllerTest < ActionDispatch::IntegrationTest + setup do + @user = users(:lazaro_nixon) + end + + test "should get index" do + sign_in_as @user + + get sessions_url + assert_response :success + end + + test "should get new" do + get sign_in_url + assert_response :success + end + + test "should sign in" do + post sign_in_url, params: { email: @user.email, password: "Secret1*3*5*" } + assert_redirected_to root_url + + get root_url + assert_response :success + end + + test "should not sign in with wrong credentials" do + post sign_in_url, params: { email: @user.email, password: "SecretWrong1*3" } + assert_redirected_to sign_in_url(email_hint: @user.email) + assert_equal "That email or password is incorrect", flash[:alert] + + get root_url + assert_redirected_to sign_in_url + end + + test "should sign out" do + sign_in_as @user + + delete session_url(@user.sessions.last) + assert_redirected_to sessions_url + + follow_redirect! + assert_redirected_to sign_in_url + end +end diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml new file mode 100644 index 0000000..4c5f5d7 --- /dev/null +++ b/test/fixtures/users.yml @@ -0,0 +1,6 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +lazaro_nixon: + email: lazaronixon@hotmail.com + password_digest: <%= BCrypt::Password.create("Secret1*3*5*") %> + verified: true diff --git a/test/mailers/user_mailer_test.rb b/test/mailers/user_mailer_test.rb new file mode 100644 index 0000000..e74b76e --- /dev/null +++ b/test/mailers/user_mailer_test.rb @@ -0,0 +1,19 @@ +require "test_helper" + +class UserMailerTest < ActionMailer::TestCase + setup do + @user = users(:lazaro_nixon) + end + + test "password_reset" do + mail = UserMailer.with(user: @user).password_reset + assert_equal "Reset your password", mail.subject + assert_equal [@user.email], mail.to + end + + test "email_verification" do + mail = UserMailer.with(user: @user).email_verification + assert_equal "Verify your email", mail.subject + assert_equal [@user.email], mail.to + end +end diff --git a/test/system/identity/emails_test.rb b/test/system/identity/emails_test.rb new file mode 100644 index 0000000..c681a77 --- /dev/null +++ b/test/system/identity/emails_test.rb @@ -0,0 +1,26 @@ +require "application_system_test_case" + +class Identity::EmailsTest < ApplicationSystemTestCase + setup do + @user = sign_in_as(users(:lazaro_nixon)) + end + + test "updating the email" do + click_on "Change email address" + + fill_in "New email", with: "new_email@hey.com" + fill_in "Password challenge", with: "Secret1*3*5*" + click_on "Save changes" + + assert_text "Your email has been changed" + end + + test "sending a verification email" do + @user.update! verified: false + + click_on "Change email address" + click_on "Re-send verification email" + + assert_text "We sent a verification email to your email address" + end +end diff --git a/test/system/identity/password_resets_test.rb b/test/system/identity/password_resets_test.rb new file mode 100644 index 0000000..1c254ab --- /dev/null +++ b/test/system/identity/password_resets_test.rb @@ -0,0 +1,28 @@ +require "application_system_test_case" + +class Identity::PasswordResetsTest < ApplicationSystemTestCase + setup do + @user = users(:lazaro_nixon) + @sid = @user.generate_token_for(:password_reset) + end + + test "sending a password reset email" do + visit sign_in_url + click_on "Forgot your password?" + + fill_in "Email", with: @user.email + click_on "Send password reset email" + + assert_text "Check your email for reset instructions" + end + + test "updating password" do + visit edit_identity_password_reset_url(sid: @sid) + + fill_in "New password", with: "Secret6*4*2*" + fill_in "Confirm new password", with: "Secret6*4*2*" + click_on "Save changes" + + assert_text "Your password was reset successfully. Please sign in" + end +end diff --git a/test/system/passwords_test.rb b/test/system/passwords_test.rb new file mode 100644 index 0000000..486fdf9 --- /dev/null +++ b/test/system/passwords_test.rb @@ -0,0 +1,18 @@ +require "application_system_test_case" + +class PasswordsTest < ApplicationSystemTestCase + setup do + @user = sign_in_as(users(:lazaro_nixon)) + end + + test "updating the password" do + click_on "Change password" + + fill_in "Password challenge", with: "Secret1*3*5*" + fill_in "New password", with: "Secret6*4*2*" + fill_in "Confirm new password", with: "Secret6*4*2*" + click_on "Save changes" + + assert_text "Your password has been changed" + end +end diff --git a/test/system/registrations_test.rb b/test/system/registrations_test.rb new file mode 100644 index 0000000..da7c0db --- /dev/null +++ b/test/system/registrations_test.rb @@ -0,0 +1,14 @@ +require "application_system_test_case" + +class RegistrationsTest < ApplicationSystemTestCase + test "signing up" do + visit sign_up_url + + fill_in "Email", with: "lazaronixon@hey.com" + fill_in "Password", with: "Secret6*4*2*" + fill_in "Password confirmation", with: "Secret6*4*2*" + click_on "Sign up" + + assert_text "Welcome! You have signed up successfully" + end +end diff --git a/test/system/sessions_test.rb b/test/system/sessions_test.rb new file mode 100644 index 0000000..640c626 --- /dev/null +++ b/test/system/sessions_test.rb @@ -0,0 +1,30 @@ +require "application_system_test_case" + +class SessionsTest < ApplicationSystemTestCase + setup do + @user = users(:lazaro_nixon) + end + + test "visiting the index" do + sign_in_as @user + + click_on "Devices & Sessions" + assert_selector "h1", text: "Sessions" + end + + test "signing in" do + visit sign_in_url + fill_in "Email", with: @user.email + fill_in "Password", with: "Secret1*3*5*" + click_on "Sign in" + + assert_text "Signed in successfully" + end + + test "signing out" do + sign_in_as @user + + click_on "Log out" + assert_text "That session has been logged out" + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 0c22470..3631664 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -2,14 +2,15 @@ ENV["RAILS_ENV"] ||= "test" require_relative "../config/environment" require "rails/test_help" -module ActiveSupport - class TestCase - # Run tests in parallel with specified workers - parallelize(workers: :number_of_processors) +class ActiveSupport::TestCase + # Run tests in parallel with specified workers + parallelize(workers: :number_of_processors) - # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. - fixtures :all + # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. + fixtures :all - # Add more helper methods to be used by all tests here... + # Add more helper methods to be used by all tests here... + def sign_in_as(user) + post(sign_in_url, params: { email: user.email, password: "Secret1*3*5*" }); user end end