diff --git a/lib/generators/rspec/authentication/authentication_generator.rb b/lib/generators/rspec/authentication/authentication_generator.rb index f653c5c4e..9fda14859 100644 --- a/lib/generators/rspec/authentication/authentication_generator.rb +++ b/lib/generators/rspec/authentication/authentication_generator.rb @@ -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 @@ -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 diff --git a/lib/generators/rspec/authentication/templates/authentication_support.rb b/lib/generators/rspec/authentication/templates/authentication_support.rb new file mode 100644 index 000000000..2812cb6e1 --- /dev/null +++ b/lib/generators/rspec/authentication/templates/authentication_support.rb @@ -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 diff --git a/lib/generators/rspec/authentication/templates/password_spec.rb b/lib/generators/rspec/authentication/templates/password_spec.rb new file mode 100644 index 000000000..70f354afa --- /dev/null +++ b/lib/generators/rspec/authentication/templates/password_spec.rb @@ -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 diff --git a/lib/generators/rspec/authentication/templates/session_spec.rb b/lib/generators/rspec/authentication/templates/session_spec.rb new file mode 100644 index 000000000..46076915f --- /dev/null +++ b/lib/generators/rspec/authentication/templates/session_spec.rb @@ -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 diff --git a/spec/generators/rspec/authentication/authentication_generator_spec.rb b/spec/generators/rspec/authentication/authentication_generator_spec.rb index e5590aedd..671c60268 100644 --- a/spec/generators/rspec/authentication/authentication_generator_spec.rb +++ b/spec/generators/rspec/authentication/authentication_generator_spec.rb @@ -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 @@ -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']