Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions lib/generators/rspec/authentication/authentication_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ module Rspec
module Generators
# @private
class AuthenticationGenerator < Base
class_option :request_specs, type: :boolean, default: true, desc: 'Generate request specs'

def initialize(args, *options)
args.replace(['User'])
super
Expand All @@ -20,6 +22,35 @@ def create_fixture_file

template 'users.yml', target_path('fixtures', 'users.yml')
end

def create_session_request_spec
return unless options[:request_specs]

template 'session_spec.rb', target_path('requests', 'sessions_spec.rb')
end

def create_password_request_spec
return unless options[:request_specs]

template 'password_spec.rb', target_path('requests', 'passwords_spec.rb')
end

def create_authentication_support
template 'authentication_support.rb', target_path('support', 'authentication_support.rb')
end

def configure_authentication_support
rails_helper_path = File.join(destination_root, 'spec', 'rails_helper.rb')
return unless File.exist?(rails_helper_path)

# Uncomment the support files loading line if it's commented out
uncomment_lines rails_helper_path, /Rails\.root\.glob\('spec\/support\/\*\*\/\*\.rb'\)\.sort_by\(&:to_s\)\.each \{ \|f\| require f \}/

include_statement = " # Include authentication support module for request specs\n config.include AuthenticationSupport, type: :request\n"

# Insert the include statement before the final 'end' of the RSpec.configure block
inject_into_file rails_helper_path, include_statement, before: /^end\s*$/
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module AuthenticationSupport
# Helper method to sign in a user for testing purposes
# Uses the actual authentication flow via POST request
def sign_in_as(user)
post session_path, params: {
email_address: user.email_address,
password: "password"
}
end
end
79 changes: 79 additions & 0 deletions lib/generators/rspec/authentication/templates/password_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
require 'rails_helper'

RSpec.describe "Passwords", <%= type_metatag(:request) %> do
# TODO: Replace with your factory or model creation method
# For example, with FactoryBot: let(:user) { create(:user) }
# or with fixtures: let(:user) { users(:one) }
let(:user) { User.create!(email_address: "test@example.com", password: "password") }

describe "GET /password/new" do
it "returns http success" do
get new_password_path
expect(response).to have_http_status(:success)
end
end

describe "POST /password" do
it "sends password reset email for valid user" do
expect {
post passwords_path, params: { email_address: user.email_address }
}.to have_enqueued_mail(PasswordsMailer, :reset).with(user)

expect(response).to redirect_to(new_session_path)

follow_redirect!
expect(flash[:notice]).to eq("Password reset instructions sent (if user with that email address exists).")
end

it "handles invalid email gracefully" do
expect {
post passwords_path, params: { email_address: "missing-user@example.com" }
}.not_to have_enqueued_mail

expect(response).to redirect_to(new_session_path)

follow_redirect!
expect(flash[:notice]).to eq("Password reset instructions sent (if user with that email address exists).")
end
end

describe "GET /password/edit" do
it "returns http success with valid token" do
get edit_password_path(user.password_reset_token)
expect(response).to have_http_status(:success)
end

it "redirects with invalid password reset token" do
get edit_password_path("invalid token")
expect(response).to redirect_to(new_password_path)

follow_redirect!
expect(flash[:alert]).to eq("Password reset link is invalid or has expired.")
end
end

describe "PATCH /password" do
it "updates password with valid token and password" do
expect {
patch password_path(user.password_reset_token), params: { password: "new", password_confirmation: "new" }
}.to change { user.reload.password_digest }

expect(response).to redirect_to(new_session_path)

follow_redirect!
expect(flash[:notice]).to eq("Password has been reset.")
end

it "rejects non matching passwords" do
token = user.password_reset_token
expect {
patch password_path(token), params: { password: "no", password_confirmation: "match" }
}.not_to change { user.reload.password_digest }

expect(response).to redirect_to(edit_password_path(token))

follow_redirect!
expect(flash[:alert]).to eq("Passwords did not match.")
end
end
end
47 changes: 47 additions & 0 deletions lib/generators/rspec/authentication/templates/session_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
require 'rails_helper'

RSpec.describe "Sessions", <%= type_metatag(:request) %> do
# TODO: Replace with your factory or model creation method
# For example, with FactoryBot: let(:user) { create(:user) }
# or with fixtures: let(:user) { users(:one) }
let(:user) { User.create!(email_address: "test@example.com", password: "password") }

describe "GET /new_session" do
it "returns http success" do
get new_session_path
expect(response).to have_http_status(:success)
end
end

describe "POST /session" do
context "with valid credentials" do
it "redirects to root path and sets session cookie" do
post session_path, params: { email_address: user.email_address, password: "password" }

expect(response).to redirect_to(root_path)
expect(cookies[:session_id]).to be_present
end
end

context "with invalid credentials" do
it "redirects to new session path and does not set session cookie" do
post session_path, params: { email_address: user.email_address, password: "wrong" }

expect(response).to redirect_to(new_session_path)
expect(cookies[:session_id]).to be_nil
end
end
end

describe "DELETE /session" do
it "logs out the current user and redirects to new session path" do
# Simulate being signed in
sign_in_as(user)

delete session_path

expect(response).to redirect_to(new_session_path)
expect(cookies[:session_id]).to be_empty
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@
RSpec.describe Rspec::Generators::AuthenticationGenerator, type: :generator do
setup_default_destination

it 'runs both the model and fixture tasks' do
it 'runs the model, fixture, and request spec tasks' do
gen = generator
expect(gen).to receive :create_user_spec
expect(gen).to receive :create_fixture_file
expect(gen).to receive :create_session_request_spec
expect(gen).to receive :create_password_request_spec
expect(gen).to receive :create_authentication_support
expect(gen).to receive :configure_authentication_support
gen.invoke_all
end

Expand All @@ -19,6 +23,48 @@
expect(File.exist?(file('spec/models/user_spec.rb'))).to be true
end

it 'creates the request specs' do
run_generator

expect(File.exist?(file('spec/requests/sessions_spec.rb'))).to be true
expect(File.exist?(file('spec/requests/passwords_spec.rb'))).to be true
end

it 'configures the authentication support' do
# Create a minimal rails_helper.rb file that the generator can modify
FileUtils.mkdir_p(File.join(destination_root, 'spec'))
rails_helper_content = <<~CONTENT
# Rails.root.glob('spec/support/**/*.rb').sort_by(&:to_s).each { |f| require f }
RSpec.configure do |config|
end
CONTENT
File.write(File.join(destination_root, 'spec', 'rails_helper.rb'), rails_helper_content)

run_generator

expect(file('spec/rails_helper.rb')).to contain(
" # Include authentication support module for request specs\n config.include AuthenticationSupport, type: :request\n"
)
expect(file('spec/rails_helper.rb')).to contain(
/^#{Regexp.escape("Rails.root.glob('spec/support/**/*.rb').sort_by(&:to_s).each { |f| require f }")}$/
)

expect(File.exist?(file('spec/support/authentication_support.rb'))).to be true
end

describe 'with request specs disabled' do
before do
run_generator ['--request-specs=false']
end

describe 'the request specs' do
it "will skip the files" do
expect(File.exist?(file('spec/requests/sessions_spec.rb'))).to be false
expect(File.exist?(file('spec/requests/passwords_spec.rb'))).to be false
end
end
end

describe 'with fixture replacement' do
before do
run_generator ['--fixture-replacement=factory_bot']
Expand Down
Loading